Repository: pypa/pip

Issue:
Sorting TypeError in move_wheel_files() during install (e.g. Poetry)
**Environment**

* pip version: Latest
* Python version: 3.6.2
* OS: Fedora 25

<!-- Feel free to add more information about your environment here -->

**Description**
<!-- A clear and concise description of what the bug is. -->
I'm getting the exception:
```
TypeError: '<' not supported between instances of 'int' and 'str'
```
when attempting to install poetry (example dockerfile and full stacktrace below).
Looking at the code for pip, in `move_wheel_files` it calls `sorted(outrows)` which is sorting a tuple. The 3rd column for that tuple looks like it could be an int or string, which is a bug:
```
>>> sorted((('','',''),('','',1)))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: '<' not supported between instances of 'int' and 'str'
```

so the code in pip thats relevant:
```
            outrows = []
            for row in reader:
                row[0] = installed.pop(row[0], row[0])
                if row[0] in changed:
                    row[1], row[2] = rehash(row[0])
                outrows.append(tuple(row))
            for f in generated:
                digest, length = rehash(f)
                outrows.append((normpath(f, lib_dir), digest, length))
            for f in installed:
                outrows.append((installed[f], '', ''))
            for row in sorted(outrows):
                writer.writerow(row)
```
as you can see, the `for f in installed` will always place a string in the 3rd column of the tuple, however, the paths that use rehash put `length` in the 3rd column which looking at the code for rehash, will always be an integer:

```
def rehash(path, blocksize=1 << 20):
    """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)
    digest = 'sha256=' + urlsafe_b64encode(
        h.digest()
    ).decode('latin1').rstrip('=')
    return (digest, length)
```

**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
Not error out on sorting

**How to Reproduce**
<!-- Describe the steps to reproduce this bug. -->

Example dockerfile that reproduces the issue:
```
FROM fedora:25

RUN dnf -y update
RUN dnf -y install python36
RUN bash -c "curl https://bootstrap.pypa.io/get-pip.py | python3.6"
RUN bash -c "curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python3.6"
```

**Output**

```
$ docker build -t poetry_fail .
Sending build context to Docker daemon    554kB
Step 1/5 : FROM fedora:25
 ---> 9cffd21a45e3
Step 2/5 : RUN dnf -y update
 ---> Using cache
 ---> 9afadb458128
Step 3/5 : RUN dnf -y install python36
 ---> Using cache
 ---> 7291182779ff
Step 4/5 : RUN bash -c "curl https://bootstrap.pypa.io/get-pip.py | python3.6"
 ---> Using cache
 ---> debf1591f40a
Step 5/5 : RUN bash -c "curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python3.6"
 ---> Running in 04bb671e7e1e
Retrieving metadata

Installing version: 0.11.5
  - Getting dependencies
  - Vendorizing dependencies
  - Installing poetry
An error has occured: Command '('/usr/bin/python3.6', '-m', 'pip', 'install', '--upgrade', '--no-deps', '/tmp/poetry-installer-t434oaug/poetry-0.11.5-py2.py3-none-any.whl')' returned non-zero exit status 2.
Processing /tmp/poetry-installer-t434oaug/poetry-0.11.5-py2.py3-none-any.whl
Installing collected packages: poetry
Exception:
Traceback (most recent call last):
  File "/usr/lib/python3.6/site-packages/pip/_internal/cli/base_command.py", line 143, in main
    status = self.run(options, args)
  File "/usr/lib/python3.6/site-packages/pip/_internal/commands/install.py", line 366, in run
    use_user_site=options.use_user_site,
  File "/usr/lib/python3.6/site-packages/pip/_internal/req/__init__.py", line 49, in install_given_reqs
    **kwargs
  File "/usr/lib/python3.6/site-packages/pip/_internal/req/req_install.py", line 760, in install
    use_user_site=use_user_site, pycompile=pycompile,
  File "/usr/lib/python3.6/site-packages/pip/_internal/req/req_install.py", line 382, in move_wheel_files
    warn_script_location=warn_script_location,
  File "/usr/lib/python3.6/site-packages/pip/_internal/wheel.py", line 514, in move_wheel_files
    for row in sorted(outrows):
TypeError: '<' not supported between instances of 'int' and 'str'

The command '/bin/sh -c bash -c "curl -sSL https://raw.githubusercontent.com/sdispater/poetry/master/get-poetry.py | python3.6"' returned a non-zero code: 2
```



Discussion:
Issue caused by https://github.com/pypa/pip/commit/f4bda78815bc9144255dc5ed29a89d97a54b131a -- looks like the rehash length should just be cast to str() so that there are consistent types?
having the same issue when trying to install poetry on CircleCI
cc @xavfernandez 

Resolving patch:
diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py
--- a/src/pip/_internal/wheel.py
+++ b/src/pip/_internal/wheel.py
@@ -204,6 +204,26 @@ def message_about_scripts_not_on_PATH(scripts):
     return "\n".join(msg_lines)
 
 
+def sorted_outrows(outrows):
+    """
+    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
+    passed to this function, the size can be an integer as an int or string,
+    or the empty string.
+    """
+    # Normally, there should only be one row per path, in which case the
+    # second and third elements don't come into play when sorting.
+    # However, in cases in the wild where a path might happen to occur twice,
+    # we don't want the sort operation to trigger an error (but still want
+    # determinism).  Since the third element can be an int or string, we
+    # 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))
+
+
 def move_wheel_files(name, req, wheeldir, user=False, home=None, root=None,
                      pycompile=True, scheme=None, isolated=False, prefix=None,
                      warn_script_location=True):
@@ -511,7 +531,8 @@ def _get_script_text(entry):
                 outrows.append((normpath(f, lib_dir), digest, length))
             for f in installed:
                 outrows.append((installed[f], '', ''))
-            for row in sorted(outrows):
+            # Sort to simplify testing.
+            for row in sorted_outrows(outrows):
                 writer.writerow(row)
     shutil.move(temp_record, record)
 


Test patch:
diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py
--- a/tests/unit/test_wheel.py
+++ b/tests/unit/test_wheel.py
@@ -33,6 +33,29 @@ def test_get_entrypoints(tmpdir, console_scripts):
     )
 
 
+@pytest.mark.parametrize("outrows, expected", [
+    ([
+        ('', '', 'a'),
+        ('', '', ''),
+    ], [
+        ('', '', ''),
+        ('', '', 'a'),
+    ]),
+    ([
+        # Include an int to check avoiding the following error:
+        # > TypeError: '<' not supported between instances of 'str' and 'int'
+        ('', '', 1),
+        ('', '', ''),
+    ], [
+        ('', '', ''),
+        ('', '', 1),
+    ]),
+])
+def test_sorted_outrows(outrows, expected):
+    actual = wheel.sorted_outrows(outrows)
+    assert actual == expected
+
+
 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'