Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix pip-sync to check hashes #706

Merged
merged 2 commits into from
Jan 15, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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):
atugushev marked this conversation as resolved.
Show resolved Hide resolved
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