Repository: jazzband/pip-tools

Issue:
pip 20.0.2 does not support relative paths for cache dir
In [this PR](https://github.com/pypa/pip/pull/7542) `pip` starts relying on path normalization outside of `WheelCache` class, while it is still being [instantiated manually](https://github.com/jazzband/pip-tools/blob/master/piptools/repositories/pypi.py#L287) by `pip-tools` without path normalization.

Using normalized path should be backward compatible with older versions of pip as well, so it should be safe to do path normalization on `pip-tools` side.

### Steps to reproduce:
```bash
> python3 -m venv venv
> venv/bin/pip install -U pip pip-tools
Collecting pip
  Downloading https://files.pythonhosted.org/packages/54/0c/d01aa759fdc501a58f431eb594a17495f15b88da142ce14b5845662c13f3/pip-20.0.2-py2.py3-none-any.whl (1.4MB)
     |████████████████████████████████| 1.4MB 3.8MB/s 
Collecting pip-tools
  Downloading https://files.pythonhosted.org/packages/db/e3/e9c864e62c5e61f1fa6c3b6232cef135ce7365ceb4b1cfa145e47753ab07/pip_tools-4.4.1-py2.py3-none-any.whl (41kB)
     |████████████████████████████████| 51kB 47.7MB/s 
Collecting six (from pip-tools)
  Downloading https://files.pythonhosted.org/packages/65/eb/1f97cb97bfc2390a276969c6fae16075da282f5058082d4cb10c6c5c1dba/six-1.14.0-py2.py3-none-any.whl
Collecting click>=7 (from pip-tools)
  Downloading https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl (81kB)
     |████████████████████████████████| 81kB 52.6MB/s 
Installing collected packages: pip, six, click, pip-tools
  Found existing installation: pip 19.2.3
    Uninstalling pip-19.2.3:
      Successfully uninstalled pip-19.2.3
Successfully installed click-7.0 pip-20.0.2 pip-tools-4.4.1 six-1.14.0
> env XDG_CACHE_HOME='.cache' venv/bin/pip-compile --build-isolation reqs.in --output-file reqs.txt
Traceback (most recent call last):
  File "venv/bin/pip-compile", line 10, in <module>
    sys.exit(cli())
  File "venv/lib/python3.8/site-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "venv/lib/python3.8/site-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "venv/lib/python3.8/site-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "venv/lib/python3.8/site-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "venv/lib/python3.8/site-packages/click/decorators.py", line 17, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "venv/lib/python3.8/site-packages/piptools/scripts/compile.py", line 275, in cli
    repository = PyPIRepository(
  File "venv/lib/python3.8/site-packages/piptools/repositories/pypi.py", line 66, in __init__
    self.session = self.command._build_session(self.options)
  File "venv/lib/python3.8/site-packages/pip/_internal/cli/req_command.py", line 83, in _build_session
    assert not options.cache_dir or os.path.isabs(options.cache_dir)
AssertionError
```


Discussion:
Hello @kammala,

Thanks for the issue and reproducible steps! You have already researched the bug, so nicely 🙏 
was playing around and found another way to get into similar trouble using `PIP_TOOLS_CACHE_DIR` instead of `XDG_CACHE_DIR`:
```bash
> env PIP_TOOLS_CACHE_DIR='.cache' venv/bin/pip-compile --build-isolation reqs.in --output-file reqs.txt
Traceback (most recent call last):
  File "venv/bin/pip-compile", line 11, in <module>
    load_entry_point('pip-tools', 'console_scripts', 'pip-compile')()
  File "venv/lib/python3.8/site-packages/click/core.py", line 764, in __call__
    return self.main(*args, **kwargs)
  File "venv/lib/python3.8/site-packages/click/core.py", line 717, in main
    rv = self.invoke(ctx)
  File "venv/lib/python3.8/site-packages/click/core.py", line 956, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "venv/lib/python3.8/site-packages/click/core.py", line 555, in invoke
    return callback(*args, **kwargs)
  File "venv/lib/python3.8/site-packages/click/decorators.py", line 17, in new_func
    return f(get_current_context(), *args, **kwargs)
  File "pip-tools/piptools/scripts/compile.py", line 385, in cli
    results = resolver.resolve(max_rounds=max_rounds)
  File "pip-tools/piptools/resolver.py", line 165, in resolve
    has_changed, best_matches = self._resolve_one_round()
  File "pip-tools/piptools/resolver.py", line 260, in _resolve_one_round
    their_constraints.extend(self._iter_dependencies(best_match))
  File "pip-tools/piptools/resolver.py", line 366, in _iter_dependencies
    dependencies = self.repository.get_dependencies(ireq)
  File "pip-tools/piptools/repositories/pypi.py", line 287, in get_dependencies
    wheel_cache = WheelCache(self._cache_dir, self.options.format_control)
  File "pip-tools/venv/lib/python3.8/site-packages/pip/_internal/cache.py", line 287, in __init__
    super(WheelCache, self).__init__(
  File "pip-tools/venv/lib/python3.8/site-packages/pip/_internal/cache.py", line 53, in __init__
    assert not cache_dir or os.path.isabs(cache_dir)
AssertionError
```

Resolving patch:
diff --git a/piptools/_compat/__init__.py b/piptools/_compat/__init__.py
--- a/piptools/_compat/__init__.py
+++ b/piptools/_compat/__init__.py
@@ -28,6 +28,7 @@
     is_dir_url,
     is_file_url,
     is_vcs_url,
+    normalize_path,
     parse_requirements,
     path_to_url,
     stdlib_pkgs,
diff --git a/piptools/_compat/pip_compat.py b/piptools/_compat/pip_compat.py
--- a/piptools/_compat/pip_compat.py
+++ b/piptools/_compat/pip_compat.py
@@ -72,6 +72,7 @@ def do_import(module_path, subimport=None, old_path=None):
 Session = do_import("_vendor.requests.sessions", "Session")
 Resolver = do_import("legacy_resolve", "Resolver", old_path="resolve")
 WheelCache = do_import("cache", "WheelCache", old_path="wheel")
+normalize_path = do_import("utils.misc", "normalize_path", old_path="utils")
 
 # pip 18.1 has refactored InstallRequirement constructors use by pip-tools.
 if PIP_VERSION < (18, 1):
diff --git a/piptools/repositories/pypi.py b/piptools/repositories/pypi.py
--- a/piptools/repositories/pypi.py
+++ b/piptools/repositories/pypi.py
@@ -24,6 +24,7 @@
     is_dir_url,
     is_file_url,
     is_vcs_url,
+    normalize_path,
     path_to_url,
     url_to_path,
 )
@@ -62,6 +63,8 @@ def __init__(self, pip_args, cache_dir, build_isolation=False):
         # and pre) are deferred to pip.
         self.command = create_install_command()
         self.options, _ = self.command.parse_args(pip_args)
+        if self.options.cache_dir:
+            self.options.cache_dir = normalize_path(self.options.cache_dir)
 
         self.session = self.command._build_session(self.options)
         self.finder = self.command._build_package_finder(
@@ -81,7 +84,7 @@ def __init__(self, pip_args, cache_dir, build_isolation=False):
 
         # Setup file paths
         self.freshen_build_caches()
-        self._cache_dir = cache_dir
+        self._cache_dir = normalize_path(cache_dir)
         self._download_dir = fs_str(os.path.join(self._cache_dir, "pkgs"))
         self._wheel_download_dir = fs_str(os.path.join(self._cache_dir, "wheels"))
 


Test patch:
diff --git a/tests/test_repository_pypi.py b/tests/test_repository_pypi.py
--- a/tests/test_repository_pypi.py
+++ b/tests/test_repository_pypi.py
@@ -1,8 +1,11 @@
+import os
+
 import mock
 import pytest
 
 from piptools._compat import PackageFinder
 from piptools._compat.pip_compat import PIP_VERSION, Link, Session, path_to_url
+from piptools.repositories import PyPIRepository
 from piptools.repositories.pypi import open_local_or_remote_file
 
 
@@ -176,3 +179,29 @@ def test_wheel_cache_cleanup_called(
     ireq = from_line("six==1.10.0")
     pypi_repository.get_dependencies(ireq)
     WheelCache.return_value.cleanup.assert_called_once_with()
+
+
+def test_relative_path_cache_dir_is_normalized(from_line):
+    relative_cache_dir = "pypi-repo-cache"
+    pypi_repository = PyPIRepository([], cache_dir=relative_cache_dir)
+
+    assert os.path.isabs(pypi_repository._cache_dir)
+    assert pypi_repository._cache_dir.endswith(relative_cache_dir)
+
+
+def test_relative_path_pip_cache_dir_is_normalized(from_line, tmpdir):
+    relative_cache_dir = "pip-cache"
+    pypi_repository = PyPIRepository(
+        ["--cache-dir", relative_cache_dir], cache_dir=str(tmpdir / "pypi-repo-cache")
+    )
+
+    assert os.path.isabs(pypi_repository.options.cache_dir)
+    assert pypi_repository.options.cache_dir.endswith(relative_cache_dir)
+
+
+def test_pip_cache_dir_is_empty(from_line, tmpdir):
+    pypi_repository = PyPIRepository(
+        ["--no-cache-dir"], cache_dir=str(tmpdir / "pypi-repo-cache")
+    )
+
+    assert not pypi_repository.options.cache_dir