Repository: jazzband/pip-tools (python)

Issue:
pip: function is_vcs_url moved to is_vcs Link property
Need to add compatibility with https://github.com/pypa/pip/pull/6883

Pull request:
Add compatibility with the pip master (upcoming pip==19.3)
* Added `create_command` function to instantiate a command (e.g., InstallCommand), see  https://github.com/pypa/pip/pull/6694.
* Added compatible `is_vcs_url` function, see https://github.com/pypa/pip/pull/6883, closes #876.
* Added compatibility for refactored `PackageFinder`, see https://github.com/pypa/pip/pull/6787.
* Added compatibility for refactored `Resolver`, see https://github.com/pypa/pip/pull/6986.


**Changelog-friendly one-liner**: Add compatibility with `pip==19.3`

##### Contributor checklist

- [X] Provided the tests for the changes.
- [x] Requested a review from another contributor.
- [X] Gave a clear one-line description in the PR (that the maintainers can add to CHANGELOG.md on release).
- [x] Assign the PR to an existing or new milestone for the target version (following [Semantic Versioning](https://blog.versioneye.com/2014/01/16/semantic-versioning/)).

Patch:
diff --git a/.appveyor.yml b/.appveyor.yml
index 76325ab..a74f6ee 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -17,6 +17,8 @@ environment:
           PIP: 19.0.3
         - TOXENV: py27-pip19.1
           PIP: 19.1
+        - TOXENV: py27-pip19.3
+          PIP: 19.3
         - TOXENV: py27-pipmaster
           PIP: master
         - TOXENV: py27-piplatest-coverage
@@ -36,6 +38,8 @@ environment:
           PIP: 19.0.3
         - TOXENV: py35-pip19.1
           PIP: 19.1
+        - TOXENV: py35-pip19.3
+          PIP: 19.3
         - TOXENV: py35-pipmaster
           PIP: master
         - TOXENV: py35-piplatest
@@ -55,6 +59,8 @@ environment:
           PIP: 19.0.3
         - TOXENV: py36-pip19.1
           PIP: 19.1
+        - TOXENV: py36-pip19.3
+          PIP: 19.3
         - TOXENV: py36-pipmaster
           PIP: master
         - TOXENV: py36-piplatest
@@ -74,6 +80,8 @@ environment:
           PIP: 19.0.3
         - TOXENV: py37-pip19.1-coverage
           PIP: 19.1
+        - TOXENV: py37-pip19.3
+          PIP: 19.3
         - TOXENV: py37-pipmaster-coverage
           PIP: master
         - TOXENV: py37-piplatest-coverage
diff --git a/.travis.yml b/.travis.yml
index 485ea51..14821d4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -16,6 +16,7 @@ env:
   - PIP=18.0
   - PIP=19.0.3
   - PIP=19.1
+  - PIP=19.3
   - PIP=latest
   - PIP=master
 
diff --git a/piptools/_compat/__init__.py b/piptools/_compat/__init__.py
index d9e881d..a58711d 100644
--- a/piptools/_compat/__init__.py
+++ b/piptools/_compat/__init__.py
@@ -7,6 +7,7 @@ import six
 from .pip_compat import (
     DEV_PKGS,
     FAVORITE_HASH,
+    PIP_VERSION,
     FormatControl,
     InstallationCandidate,
     InstallCommand,
diff --git a/piptools/_compat/pip_compat.py b/piptools/_compat/pip_compat.py
index cc34e51..8699562 100644
--- a/piptools/_compat/pip_compat.py
+++ b/piptools/_compat/pip_compat.py
@@ -4,6 +4,8 @@ import importlib
 import pip
 from pip._vendor.packaging.version import parse as parse_version
 
+PIP_VERSION = tuple(map(int, parse_version(pip.__version__).base_version.split(".")))
+
 
 def do_import(module_path, subimport=None, old_path=None):
     old_path = old_path or module_path
@@ -34,13 +36,12 @@ user_cache_dir = do_import("utils.appdirs", "user_cache_dir")
 FAVORITE_HASH = do_import("utils.hashes", "FAVORITE_HASH")
 is_file_url = do_import("download", "is_file_url")
 is_dir_url = do_import("download", "is_dir_url")
-is_vcs_url = do_import("download", "is_vcs_url")
 path_to_url = do_import("download", "path_to_url")
 url_to_path = do_import("download", "url_to_path")
 PackageFinder = do_import("index", "PackageFinder")
 FormatControl = do_import("index", "FormatControl")
-Wheel = do_import("wheel", "Wheel")
 InstallCommand = do_import("commands.install", "InstallCommand")
+Wheel = do_import("wheel", "Wheel")
 cmdoptions = do_import("cli.cmdoptions", old_path="cmdoptions")
 get_installed_distributions = do_import(
     "utils.misc", "get_installed_distributions", old_path="utils"
@@ -53,7 +54,7 @@ Session = do_import("_vendor.requests.sessions", "Session")
 Resolver = do_import("legacy_resolve", "Resolver", old_path="resolve")
 
 # pip 18.1 has refactored InstallRequirement constructors use by pip-tools.
-if parse_version(pip.__version__) < parse_version("18.1"):
+if PIP_VERSION < (18, 1):
     install_req_from_line = InstallRequirement.from_line
     install_req_from_editable = InstallRequirement.from_editable
 else:
@@ -61,3 +62,11 @@ else:
     install_req_from_editable = do_import(
         "req.constructors", "install_req_from_editable"
     )
+
+
+def is_vcs_url(link):
+    if PIP_VERSION < (19, 3):
+        _is_vcs_url = do_import("download", "is_vcs_url")
+        return _is_vcs_url(link)
+
+    return link.is_vcs
diff --git a/piptools/repositories/pypi.py b/piptools/repositories/pypi.py
index 3d5e35b..cdf885a 100644
--- a/piptools/repositories/pypi.py
+++ b/piptools/repositories/pypi.py
@@ -5,11 +5,11 @@ import collections
 import hashlib
 import os
 from contextlib import contextmanager
+from functools import partial
 from shutil import rmtree
 
 from .._compat import (
     FAVORITE_HASH,
-    InstallCommand,
     Link,
     PyPI,
     RequirementSet,
@@ -28,7 +28,7 @@ from ..click import progressbar
 from ..exceptions import NoCandidateFound
 from ..logging import log
 from ..utils import (
-    PIP_VERSION,
+    create_install_command,
     fs_str,
     is_pinned_requirement,
     is_url_requirement,
@@ -37,6 +37,8 @@ from ..utils import (
 )
 from .base import BaseRepository
 
+from piptools._compat.pip_compat import PIP_VERSION
+
 try:
     from pip._internal.req.req_tracker import RequirementTracker
 except ImportError:
@@ -71,7 +73,7 @@ class PyPIRepository(BaseRepository):
         # Use pip's parser for pip.conf management and defaults.
         # General options (find_links, index_url, extra_index_url, trusted_host,
         # and pre) are deferred to pip.
-        command = InstallCommand()
+        command = create_install_command()
         self.options, _ = command.parse_args(pip_args)
 
         self.session = command._build_session(self.options)
@@ -149,9 +151,15 @@ class PyPIRepository(BaseRepository):
         elif PIP_VERSION < (19, 2):
             evaluator = self.finder.candidate_evaluator
             best_candidate = evaluator.get_best_candidate(matching_candidates)
-        else:
+        elif PIP_VERSION < (19, 3):
             evaluator = self.finder.make_candidate_evaluator(ireq.name)
             best_candidate = evaluator.get_best_candidate(matching_candidates)
+        else:
+            evaluator = self.finder.make_candidate_evaluator(ireq.name)
+            best_candidate_result = evaluator.compute_best_candidate(
+                matching_candidates
+            )
+            best_candidate = best_candidate_result.best_candidate
 
         # Turn the candidate into a pinned InstallRequirement
         return make_install_requirement(
@@ -193,10 +201,20 @@ class PyPIRepository(BaseRepository):
                 "ignore_dependencies": False,
                 "ignore_requires_python": False,
                 "ignore_installed": True,
-                "isolated": False,
-                "wheel_cache": wheel_cache,
                 "use_user_site": False,
             }
+            make_install_req_kwargs = {"isolated": False, "wheel_cache": wheel_cache}
+
+            if PIP_VERSION < (19, 3):
+                resolver_kwargs.update(**make_install_req_kwargs)
+            else:
+                from pip._internal.req.constructors import install_req_from_req_string
+
+                make_install_req = partial(
+                    install_req_from_req_string, **make_install_req_kwargs
+                )
+                resolver_kwargs["make_install_req"] = make_install_req
+
             resolver = None
             preparer = None
             with RequirementTracker() as req_tracker:
diff --git a/piptools/scripts/compile.py b/piptools/scripts/compile.py
index 24d50b6..6415227 100755
--- a/piptools/scripts/compile.py
+++ b/piptools/scripts/compile.py
@@ -8,13 +8,14 @@ import tempfile
 from click.utils import safecall
 
 from .. import click
-from .._compat import InstallCommand, install_req_from_line, parse_requirements
+from .._compat import install_req_from_line, parse_requirements
 from ..exceptions import PipToolsError
 from ..logging import log
 from ..repositories import LocalRequirementsRepository, PyPIRepository
 from ..resolver import Resolver
 from ..utils import (
     UNSAFE_PACKAGES,
+    create_install_command,
     dedup,
     is_pinned_requirement,
     key_from_ireq,
@@ -26,7 +27,8 @@ DEFAULT_REQUIREMENTS_FILE = "requirements.in"
 DEFAULT_REQUIREMENTS_OUTPUT_FILE = "requirements.txt"
 
 # Get default values of the pip's options (including options from pip.conf).
-pip_defaults = InstallCommand().parser.get_default_values()
+install_command = create_install_command()
+pip_defaults = install_command.parser.get_default_values()
 
 
 @click.command()
diff --git a/piptools/utils.py b/piptools/utils.py
index af6d603..3e0d97b 100644
--- a/piptools/utils.py
+++ b/piptools/utils.py
@@ -5,16 +5,13 @@ import sys
 from collections import OrderedDict
 from itertools import chain, groupby
 
-import pip
 import six
 from click.utils import LazyFile
-from pip._vendor.packaging.version import parse as parse_version
 from six.moves import shlex_quote
 
-from ._compat import install_req_from_line
+from ._compat import PIP_VERSION, InstallCommand, install_req_from_line
 from .click import style
 
-PIP_VERSION = tuple(map(int, parse_version(pip.__version__).base_version.split(".")))
 UNSAFE_PACKAGES = {"setuptools", "distribute", "pip"}
 COMPILE_EXCLUDE_OPTIONS = {
     "--dry-run",
@@ -371,3 +368,15 @@ def get_compile_command(click_ctx):
                 )
 
     return " ".join(["pip-compile"] + sorted(left_args) + sorted(right_args))
+
+
+def create_install_command():
+    """
+    Return an instance of InstallCommand.
+    """
+    if PIP_VERSION < (19, 3):
+        return InstallCommand()
+
+    from pip._internal import create_command
+
+    return create_command("install")
diff --git a/tox.ini b/tox.ini
index 557e47e..63606e7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,7 @@
 [tox]
 envlist =
     # NOTE: keep this in sync with the env list in .travis.yml for tox-travis.
-    py{27,35,36,37,38,py,py3}-pip{8.1.1,9.0.1,9.0.3,10.0.1,18.0,19.0.3,19.1,latest,master}-coverage
+    py{27,35,36,37,38,py,py3}-pip{8.1.1,9.0.1,9.0.3,10.0.1,18.0,19.0.3,19.1,19.3,latest,master}-coverage
     checkqa
     readme
 skip_missing_interpreters = True
@@ -17,6 +17,8 @@ deps =
     pip18.0: pip==18.0
     pip19.0.3: pip==19.0.3
     pip19.1: pip==19.1
+    # TODO change it to pip==19.3 after pip being released
+    pip19.3: -e git+https://github.com/pypa/pip.git@master#egg=pip
     mock
     pytest!=5.1.2
     coverage: pytest-cov
@@ -30,6 +32,7 @@ setenv =
     pip18.0: PIP=18.0
     pip19.0.3: PIP==19.0.3
     pip19.1: PIP==19.1
+    pip19.3: PIP==19.3
 
     coverage: PYTEST_ADDOPTS=--strict --doctest-modules --cov --cov-report=term-missing {env:PYTEST_ADDOPTS:}
 commands_pre =
@@ -56,5 +59,6 @@ PIP =
     18.0: pip18.0
     19.0.3: pip19.0.3
     19.1: pip19.1
+    19.3: pip19.3
     latest: piplatest
     master: pipmaster


Test patch:
diff --git a/tests/test_cli_compile.py b/tests/test_cli_compile.py
index 0dce21b..800c100 100644
--- a/tests/test_cli_compile.py
+++ b/tests/test_cli_compile.py
@@ -10,10 +10,9 @@ from pytest import mark
 
 from .utils import invoke
 
-from piptools._compat.pip_compat import path_to_url
+from piptools._compat.pip_compat import PIP_VERSION, path_to_url
 from piptools.repositories import PyPIRepository
 from piptools.scripts.compile import cli
-from piptools.utils import PIP_VERSION
 
 TEST_DATA_PATH = os.path.join(os.path.split(__file__)[0], "test_data")
 MINIMAL_WHEELS_PATH = os.path.join(TEST_DATA_PATH, "minimal_wheels")
diff --git a/tests/test_repositories.py b/tests/test_repositories.py
index 58324b1..c5d3888 100644
--- a/tests/test_repositories.py
+++ b/tests/test_repositories.py
@@ -2,7 +2,7 @@ import pytest
 from mock import MagicMock, patch
 
 from piptools._compat import PackageFinder, install_req_from_line
-from piptools.utils import PIP_VERSION
+from piptools._compat.pip_compat import PIP_VERSION
 
 
 def test_pypirepo_build_dir_is_str(pypi_repository):
diff --git a/tests/test_utils.py b/tests/test_utils.py
index e8af292..18a65cf 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -10,6 +10,7 @@ from six.moves import shlex_quote
 from piptools.scripts.compile import cli as compile_cli
 from piptools.utils import (
     as_tuple,
+    create_install_command,
     dedup,
     flat_map,
     force_text,
@@ -324,3 +325,11 @@ def test_get_compile_command_sort_args(tmpdir_cwd):
             "--no-annotate --no-emit-trusted-host --no-index "
             "requirements.in setup.py"
         )
+
+
+def test_create_install_command():
+    """
+    Test create_install_command returns an instance of InstallCommand.
+    """
+    install_command = create_install_command()
+    assert install_command.name == "install"