Skip to content

Commit d38cead

Browse files
authored
Merge pull request #7324 from pradyunsg/refactor/require_hashes
Refactor handling of require_hashes in the Resolver+RequirementPreparer
2 parents 5935489 + 9775387 commit d38cead

File tree

5 files changed

+68
-84
lines changed

5 files changed

+68
-84
lines changed

src/pip/_internal/cli/req_command.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@ def make_requirement_preparer(
173173
req_tracker=req_tracker,
174174
session=session,
175175
finder=finder,
176+
require_hashes=options.require_hashes,
176177
)
177178

178179
@staticmethod
@@ -210,7 +211,6 @@ def make_resolver(
210211
force_reinstall=force_reinstall,
211212
upgrade_strategy=upgrade_strategy,
212213
py_version_info=py_version_info,
213-
require_hashes=options.require_hashes,
214214
)
215215

216216
def populate_requirement_set(
@@ -226,9 +226,6 @@ def populate_requirement_set(
226226
"""
227227
Marshal cmd line args into a requirement set.
228228
"""
229-
# NOTE: As a side-effect, options.require_hashes and
230-
# requirement_set.require_hashes may be updated
231-
232229
for filename in options.constraints:
233230
for req_to_add in parse_requirements(
234231
filename,
@@ -256,6 +253,7 @@ def populate_requirement_set(
256253
req_to_add.is_direct = True
257254
requirement_set.add_requirement(req_to_add)
258255

256+
# NOTE: options.require_hashes may be set if --require-hashes is True
259257
for filename in options.requirements:
260258
for req_to_add in parse_requirements(
261259
filename,
@@ -265,6 +263,14 @@ def populate_requirement_set(
265263
req_to_add.is_direct = True
266264
requirement_set.add_requirement(req_to_add)
267265

266+
# If any requirement has hash options, enable hash checking.
267+
requirements = (
268+
requirement_set.unnamed_requirements +
269+
list(requirement_set.requirements.values())
270+
)
271+
if any(req.has_hash_options for req in requirements):
272+
options.require_hashes = True
273+
268274
if not (args or options.editables or options.requirements):
269275
opts = {'name': self.name}
270276
if options.find_links:

src/pip/_internal/legacy_resolve.py

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,6 @@ def __init__(
124124
force_reinstall, # type: bool
125125
upgrade_strategy, # type: str
126126
py_version_info=None, # type: Optional[Tuple[int, ...]]
127-
require_hashes=False, # type: bool
128127
):
129128
# type: (...) -> None
130129
super(Resolver, self).__init__()
@@ -140,8 +139,6 @@ def __init__(
140139
self.preparer = preparer
141140
self.finder = finder
142141

143-
self.require_hashes_option = require_hashes
144-
145142
self.upgrade_strategy = upgrade_strategy
146143
self.force_reinstall = force_reinstall
147144
self.ignore_dependencies = ignore_dependencies
@@ -176,11 +173,6 @@ def resolve(self, requirement_set):
176173
list(requirement_set.requirements.values())
177174
)
178175

179-
require_hashes = (
180-
self.require_hashes_option or
181-
any(req.has_hash_options for req in root_reqs)
182-
)
183-
184176
# Actually prepare the files, and collect any exceptions. Most hash
185177
# exceptions cannot be checked ahead of time, because
186178
# req.populate_link() needs to be called before we can make decisions
@@ -189,9 +181,7 @@ def resolve(self, requirement_set):
189181
hash_errors = HashErrors()
190182
for req in chain(root_reqs, discovered_reqs):
191183
try:
192-
discovered_reqs.extend(
193-
self._resolve_one(requirement_set, req, require_hashes)
194-
)
184+
discovered_reqs.extend(self._resolve_one(requirement_set, req))
195185
except HashError as exc:
196186
exc.req = req
197187
hash_errors.append(exc)
@@ -273,14 +263,14 @@ def _check_skip_installed(self, req_to_install):
273263
self._set_req_to_reinstall(req_to_install)
274264
return None
275265

276-
def _get_abstract_dist_for(self, req, require_hashes):
277-
# type: (InstallRequirement, bool) -> AbstractDistribution
266+
def _get_abstract_dist_for(self, req):
267+
# type: (InstallRequirement) -> AbstractDistribution
278268
"""Takes a InstallRequirement and returns a single AbstractDist \
279269
representing a prepared variant of the same.
280270
"""
281271
if req.editable:
282272
return self.preparer.prepare_editable_requirement(
283-
req, require_hashes, self.use_user_site,
273+
req, self.use_user_site
284274
)
285275

286276
# satisfied_by is only evaluated by calling _check_skip_installed,
@@ -290,16 +280,15 @@ def _get_abstract_dist_for(self, req, require_hashes):
290280

291281
if req.satisfied_by:
292282
return self.preparer.prepare_installed_requirement(
293-
req, require_hashes, skip_reason
283+
req, skip_reason
294284
)
295285

296286
upgrade_allowed = self._is_upgrade_allowed(req)
297287

298288
# We eagerly populate the link, since that's our "legacy" behavior.
289+
require_hashes = self.preparer.require_hashes
299290
req.populate_link(self.finder, upgrade_allowed, require_hashes)
300-
abstract_dist = self.preparer.prepare_linked_requirement(
301-
req, require_hashes
302-
)
291+
abstract_dist = self.preparer.prepare_linked_requirement(req)
303292

304293
# NOTE
305294
# The following portion is for determining if a certain package is
@@ -333,7 +322,6 @@ def _resolve_one(
333322
self,
334323
requirement_set, # type: RequirementSet
335324
req_to_install, # type: InstallRequirement
336-
require_hashes, # type: bool
337325
):
338326
# type: (...) -> List[InstallRequirement]
339327
"""Prepare a single requirements file.
@@ -351,9 +339,7 @@ def _resolve_one(
351339
# register tmp src for cleanup in case something goes wrong
352340
requirement_set.reqs_to_cleanup.append(req_to_install)
353341

354-
abstract_dist = self._get_abstract_dist_for(
355-
req_to_install, require_hashes
356-
)
342+
abstract_dist = self._get_abstract_dist_for(req_to_install)
357343

358344
# Parse and return dependencies
359345
dist = abstract_dist.get_pkg_resources_distribution()

src/pip/_internal/operations/prepare.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ def __init__(
510510
req_tracker, # type: RequirementTracker
511511
session, # type: PipSession
512512
finder, # type: PackageFinder
513+
require_hashes, # type: bool
513514
):
514515
# type: (...) -> None
515516
super(RequirementPreparer, self).__init__()
@@ -543,6 +544,9 @@ def __init__(
543544
# Is build isolation allowed?
544545
self.build_isolation = build_isolation
545546

547+
# Should hash-checking be required?
548+
self.require_hashes = require_hashes
549+
546550
@property
547551
def _download_should_save(self):
548552
# type: () -> bool
@@ -560,7 +564,6 @@ def _download_should_save(self):
560564
def prepare_linked_requirement(
561565
self,
562566
req, # type: InstallRequirement
563-
require_hashes, # type: bool
564567
):
565568
# type: (...) -> AbstractDistribution
566569
"""Prepare a requirement that would be obtained from req.link
@@ -600,7 +603,7 @@ def prepare_linked_requirement(
600603
# requirements we have and raise some more informative errors
601604
# than otherwise. (For example, we can raise VcsHashUnsupported
602605
# for a VCS URL rather than HashMissing.)
603-
if require_hashes:
606+
if self.require_hashes:
604607
# We could check these first 2 conditions inside
605608
# unpack_url and save repetition of conditions, but then
606609
# we would report less-useful error messages for
@@ -620,8 +623,8 @@ def prepare_linked_requirement(
620623
# about them not being pinned.
621624
raise HashUnpinned()
622625

623-
hashes = req.hashes(trust_internet=not require_hashes)
624-
if require_hashes and not hashes:
626+
hashes = req.hashes(trust_internet=not self.require_hashes)
627+
if self.require_hashes and not hashes:
625628
# Known-good hashes are missing for this requirement, so
626629
# shim it with a facade object that will provoke hash
627630
# computation and then raise a HashMissing exception
@@ -679,7 +682,6 @@ def prepare_linked_requirement(
679682
def prepare_editable_requirement(
680683
self,
681684
req, # type: InstallRequirement
682-
require_hashes, # type: bool
683685
use_user_site, # type: bool
684686
):
685687
# type: (...) -> AbstractDistribution
@@ -690,7 +692,7 @@ def prepare_editable_requirement(
690692
logger.info('Obtaining %s', req)
691693

692694
with indent_log():
693-
if require_hashes:
695+
if self.require_hashes:
694696
raise InstallationError(
695697
'The editable requirement {} cannot be installed when '
696698
'requiring hashes, because there is no single file to '
@@ -712,7 +714,6 @@ def prepare_editable_requirement(
712714
def prepare_installed_requirement(
713715
self,
714716
req, # type: InstallRequirement
715-
require_hashes, # type: bool
716717
skip_reason # type: str
717718
):
718719
# type: (...) -> AbstractDistribution
@@ -728,7 +729,7 @@ def prepare_installed_requirement(
728729
skip_reason, req, req.satisfied_by.version
729730
)
730731
with indent_log():
731-
if require_hashes:
732+
if self.require_hashes:
732733
logger.debug(
733734
'Since it is already installed, we are trusting this '
734735
'package without checking its hash. To ensure a '

tests/functional/test_install.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import distutils
22
import glob
33
import os
4+
import re
45
import shutil
56
import ssl
67
import sys
@@ -498,6 +499,41 @@ def test_hashed_install_failure(script, tmpdir):
498499
assert len(result.files_created) == 0
499500

500501

502+
def assert_re_match(pattern, text):
503+
assert re.search(pattern, text), (
504+
"Could not find {!r} in {!r}".format(pattern, text)
505+
)
506+
507+
508+
@pytest.mark.network
509+
def test_hashed_install_failure_later_flag(script, tmpdir):
510+
with requirements_file(
511+
"blessings==1.0\n"
512+
"tracefront==0.1 --hash=sha256:somehash\n"
513+
"https://files.pythonhosted.org/packages/source/m/more-itertools/"
514+
"more-itertools-1.0.tar.gz#md5=b21850c3cfa7efbb70fd662ab5413bdd\n"
515+
"https://files.pythonhosted.org/"
516+
"packages/source/p/peep/peep-3.1.1.tar.gz\n",
517+
tmpdir,
518+
) as reqs_file:
519+
result = script.pip(
520+
"install", "-r", reqs_file.resolve(), expect_error=True
521+
)
522+
523+
assert_re_match(
524+
r'Hashes are required in --require-hashes mode, but they are '
525+
r'missing .*\n'
526+
r' https://files\.pythonhosted\.org/packages/source/p/peep/peep'
527+
r'-3\.1\.1\.tar\.gz --hash=sha256:[0-9a-f]+\n'
528+
r' blessings==1.0 --hash=sha256:[0-9a-f]+\n'
529+
r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n'
530+
r' tracefront==0.1 .*:\n'
531+
r' Expected sha256 somehash\n'
532+
r' Got [0-9a-f]+',
533+
result.stderr,
534+
)
535+
536+
501537
def test_install_from_local_directory_with_symlinks_to_directories(
502538
script, data):
503539
"""

tests/unit/test_req.py

Lines changed: 5 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ def _basic_resolver(self, finder, require_hashes=False):
7272
req_tracker=RequirementTracker(),
7373
session=PipSession(),
7474
finder=finder,
75+
require_hashes=require_hashes,
7576
)
7677
make_install_req = partial(
7778
install_req_from_req_string,
@@ -86,7 +87,6 @@ def _basic_resolver(self, finder, require_hashes=False):
8687
use_user_site=False, upgrade_strategy="to-satisfy-only",
8788
ignore_dependencies=False, ignore_installed=False,
8889
ignore_requires_python=False, force_reinstall=False,
89-
require_hashes=require_hashes,
9090
)
9191

9292
def test_no_reuse_existing_build_dir(self, data):
@@ -131,51 +131,6 @@ def test_environment_marker_extras(self, data):
131131
else:
132132
assert not reqset.has_requirement('simple')
133133

134-
@pytest.mark.network
135-
def test_missing_hash_checking(self):
136-
"""Make sure prepare_files() raises an error when a requirement has no
137-
hash in implicit hash-checking mode.
138-
"""
139-
reqset = RequirementSet()
140-
# No flags here. This tests that detection of later flags nonetheless
141-
# requires earlier packages to have hashes:
142-
reqset.add_requirement(get_processed_req_from_line(
143-
'blessings==1.0', lineno=1
144-
))
145-
# This flag activates --require-hashes mode:
146-
reqset.add_requirement(get_processed_req_from_line(
147-
'tracefront==0.1 --hash=sha256:somehash', lineno=2,
148-
))
149-
# This hash should be accepted because it came from the reqs file, not
150-
# from the internet:
151-
reqset.add_requirement(get_processed_req_from_line(
152-
'https://files.pythonhosted.org/packages/source/m/more-itertools/'
153-
'more-itertools-1.0.tar.gz#md5=b21850c3cfa7efbb70fd662ab5413bdd',
154-
lineno=3,
155-
))
156-
# The error text should list this as a URL and not `peep==3.1.1`:
157-
reqset.add_requirement(get_processed_req_from_line(
158-
'https://files.pythonhosted.org/'
159-
'packages/source/p/peep/peep-3.1.1.tar.gz',
160-
lineno=4,
161-
))
162-
finder = make_test_finder(index_urls=['https://pypi.org/simple/'])
163-
resolver = self._basic_resolver(finder)
164-
assert_raises_regexp(
165-
HashErrors,
166-
r'Hashes are required in --require-hashes mode, but they are '
167-
r'missing .*\n'
168-
r' https://files\.pythonhosted\.org/packages/source/p/peep/peep'
169-
r'-3\.1\.1\.tar\.gz --hash=sha256:[0-9a-f]+\n'
170-
r' blessings==1.0 --hash=sha256:[0-9a-f]+\n'
171-
r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n'
172-
r' tracefront==0.1 .*:\n'
173-
r' Expected sha256 somehash\n'
174-
r' Got [0-9a-f]+$',
175-
resolver.resolve,
176-
reqset
177-
)
178-
179134
def test_missing_hash_with_require_hashes(self, data):
180135
"""Setting --require-hashes explicitly should raise errors if hashes
181136
are missing.
@@ -232,7 +187,7 @@ def test_unsupported_hashes(self, data):
232187
lineno=2,
233188
))
234189
finder = make_test_finder(find_links=[data.find_links])
235-
resolver = self._basic_resolver(finder)
190+
resolver = self._basic_resolver(finder, require_hashes=True)
236191
sep = os.path.sep
237192
if sep == '\\':
238193
sep = '\\\\' # This needs to be escaped for the regex
@@ -266,7 +221,7 @@ def test_unpinned_hash_checking(self, data):
266221
lineno=2,
267222
))
268223
finder = make_test_finder(find_links=[data.find_links])
269-
resolver = self._basic_resolver(finder)
224+
resolver = self._basic_resolver(finder, require_hashes=True)
270225
assert_raises_regexp(
271226
HashErrors,
272227
# Make sure all failing requirements are listed:
@@ -285,7 +240,7 @@ def test_hash_mismatch(self, data):
285240
'%s --hash=sha256:badbad' % file_url, lineno=1,
286241
))
287242
finder = make_test_finder(find_links=[data.find_links])
288-
resolver = self._basic_resolver(finder)
243+
resolver = self._basic_resolver(finder, require_hashes=True)
289244
assert_raises_regexp(
290245
HashErrors,
291246
r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n'
@@ -301,7 +256,7 @@ def test_unhashed_deps_on_require_hashes(self, data):
301256
dependencies get complained about when --require-hashes is on."""
302257
reqset = RequirementSet()
303258
finder = make_test_finder(find_links=[data.find_links])
304-
resolver = self._basic_resolver(finder)
259+
resolver = self._basic_resolver(finder, require_hashes=True)
305260
reqset.add_requirement(get_processed_req_from_line(
306261
'TopoRequires2==0.0.1 ' # requires TopoRequires
307262
'--hash=sha256:eaf9a01242c9f2f42cf2bd82a6a848cd'

0 commit comments

Comments
 (0)