Skip to content

Commit

Permalink
Fix pip-sync to check hashes
Browse files Browse the repository at this point in the history
Under the hood the pip-sync installs packages by `pip install pkg1 pkg2 ...`,
which causes the issue with checking hashes.

Steps to fix:
- gather all InstallRequirements into a temporary requirement file
- install packages by `pip install -r temp-requirements.txt`
  • Loading branch information
atugushev committed Jan 11, 2019
1 parent b558fc8 commit 57ad548
Show file tree
Hide file tree
Showing 5 changed files with 75 additions and 25 deletions.
20 changes: 13 additions & 7 deletions piptools/sync.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import collections
import os
import sys
import tempfile
from subprocess import check_call

from . import click
from .exceptions import IncompatibleRequirements, UnsupportedConstraint
from .utils import flat_map, format_requirement, key_from_ireq, key_from_req
from .utils import flat_map, format_requirement, key_from_ireq, key_from_req, get_hashes_from_ireq

PACKAGES_TO_IGNORE = [
'-markerlib',
Expand Down Expand Up @@ -156,11 +157,16 @@ def sync(to_install, to_uninstall, verbose=False, dry_run=False, pip_flags=None,
for ireq in to_install:
click.echo(" {}".format(format_requirement(ireq)))
else:
package_args = []
# prepare requirement lines
req_lines = []
for ireq in sorted(to_install, key=key_from_ireq):
if ireq.editable:
package_args.extend(['-e', str(ireq.link or ireq.req)])
else:
package_args.append(str(ireq.req))
check_call([pip, 'install'] + pip_flags + install_flags + package_args)
ireq_hashes = get_hashes_from_ireq(ireq)
req_lines.append(format_requirement(ireq, hashes=ireq_hashes))

# save requirement lines to a temporary file
with tempfile.NamedTemporaryFile(mode='wt') as tmp_req_file:
tmp_req_file.write('\n'.join(req_lines))
tmp_req_file.flush()

check_call([pip, 'install', '-r', tmp_req_file.name] + pip_flags + install_flags)
return 0
19 changes: 18 additions & 1 deletion piptools/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def make_install_requirement(name, version, extras, constraint=False):
constraint=constraint)


def format_requirement(ireq, marker=None):
def format_requirement(ireq, marker=None, hashes=None):
"""
Generic formatter for pretty printing InstallRequirements to the terminal
in a less verbose way than using its `__str__` method.
Expand All @@ -63,6 +63,10 @@ def format_requirement(ireq, marker=None):
else:
line = str(ireq.req).lower()

if hashes:
for hash_ in sorted(hashes):
line += " \\\n --hash={}".format(hash_)

if marker:
line = '{} ; {}'.format(line, marker)

Expand Down Expand Up @@ -249,3 +253,16 @@ def temp_environ():
finally:
os.environ.clear()
os.environ.update(environ)


def get_hashes_from_ireq(ireq):
"""
Given an InstallRequirement, return a list of string hashes in the format "{algorithm}:{hash}".
Return an empty list if there are no hashes in the requirement options.
"""
result = []
ireq_hashes = ireq.options.get('hashes', {})
for algorithm, hexdigests in ireq_hashes.items():
for hash_ in hexdigests:
result.append("{}:{}".format(algorithm, hash_))
return result
7 changes: 2 additions & 5 deletions piptools/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,12 +130,9 @@ def write(self, results, unsafe_requirements, reverse_dependencies,
f.write(os.linesep.encode('utf-8'))

def _format_requirement(self, ireq, reverse_dependencies, primary_packages, marker=None, hashes=None):
line = format_requirement(ireq, marker=marker)

ireq_hashes = (hashes if hashes is not None else {}).get(ireq)
if ireq_hashes:
for hash_ in sorted(ireq_hashes):
line += " \\\n --hash={}".format(hash_)

line = format_requirement(ireq, marker=marker, hashes=ireq_hashes)

if not self.annotate or key_from_req(ireq.req) in primary_packages:
return line
Expand Down
21 changes: 10 additions & 11 deletions tests/test_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import os
import platform
import sys
import tempfile

import mock
import pytest
Expand All @@ -10,6 +11,13 @@
from piptools.sync import dependency_tree, diff, merge, sync


@pytest.fixture
def mocked_tmp_req_file():
with mock.patch.object(tempfile, 'NamedTemporaryFile') as m:
m.return_value.__enter__.return_value.name = 'requirements.txt'
yield m.return_value.__enter__.return_value


@pytest.mark.parametrize(
('installed', 'root', 'expected'),
Expand Down Expand Up @@ -218,18 +226,9 @@ def test_diff_with_editable(fake_dist, from_editable):
['django==1.8', 'click==4.0'],
]
)
def test_sync_install(from_line, lines):
def test_sync_install(from_line, lines, mocked_tmp_req_file):
with mock.patch('piptools.sync.check_call') as check_call:
to_install = {from_line(line) for line in lines}

sync(to_install, set())
check_call.assert_called_once_with(['pip', 'install', '-q'] + sorted(lines))


def test_sync_with_editable(from_editable):
with mock.patch('piptools.sync.check_call') as check_call:
path_to_package = os.path.join(os.path.dirname(__file__), 'test_data', 'small_fake_package')
to_install = {from_editable(path_to_package)}

sync(to_install, set())
check_call.assert_called_once_with(['pip', 'install', '-q', '-e', _get_file_url(path_to_package)])
check_call.assert_called_once_with(['pip', 'install', '-r', mocked_tmp_req_file.name, '-q'])
33 changes: 32 additions & 1 deletion tests/test_utils.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from pytest import raises

from piptools.utils import (
as_tuple, format_requirement, format_specifier, flat_map, dedup)
as_tuple, format_requirement, format_specifier, flat_map, dedup, get_hashes_from_ireq)


def test_format_requirement(from_line):
Expand All @@ -14,6 +14,21 @@ def test_format_requirement_editable(from_editable):
assert format_requirement(ireq) == '-e git+git://fake.org/x/y.git#egg=y'


def test_format_requirement_ireq_with_hashes(from_line):
ireq = from_line('pytz==2017.2')
ireq_hashes = [
'sha256:d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67',
'sha256:f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589',
]

expected = (
'pytz==2017.2 \\\n'
' --hash=sha256:d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67 \\\n'
' --hash=sha256:f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589'
)
assert format_requirement(ireq, hashes=ireq_hashes) == expected


def test_format_specifier(from_line):
ireq = from_line('foo')
assert format_specifier(ireq) == '<any>'
Expand Down Expand Up @@ -58,3 +73,19 @@ def test_flat_map():

def test_dedup():
assert list(dedup([3, 1, 2, 4, 3, 5])) == [3, 1, 2, 4, 5]


def test_get_hashes_from_ireq(from_line):
ireq = from_line('pytz==2017.2', options={
'hashes': {
'sha256': [
'd1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67',
'f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589'
]
}
})
expected = [
'sha256:d1d6729c85acea5423671382868627129432fba9a89ecbb248d8d1c7a9f01c67',
'sha256:f5c056e8f62d45ba8215e5cb8f50dfccb198b4b9fbea8500674f3443e4689589',
]
assert get_hashes_from_ireq(ireq) == expected

0 comments on commit 57ad548

Please sign in to comment.