Skip to content

Commit

Permalink
Teach requirements parser how to parser hash options, like --sha256.
Browse files Browse the repository at this point in the history
We purposely keep it off the CLI for now. optparse isn't really geared to expose interspersed args and options, so a more heavy-handed approach will be necessary to support things like `pip install SomePackage --sha256=abcdef... OtherPackage --sha256=012345...`.
  • Loading branch information
erikrose committed Sep 23, 2015
1 parent 9211d6e commit 3303be0
Show file tree
Hide file tree
Showing 3 changed files with 58 additions and 1 deletion.
39 changes: 39 additions & 0 deletions pip/cmdoptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from __future__ import absolute_import

from functools import partial
import hashlib
from optparse import OptionGroup, SUPPRESS_HELP, Option
import warnings

Expand Down Expand Up @@ -522,6 +523,44 @@ def only_binary():
help=SUPPRESS_HELP,
)

def _good_hashes():
"""Return names of hashlib algorithms at least as strong as sha256.
Preserve the order from hashlib.algorithms so --help comes out in
deterministic order.
"""
# Remove getattr when 2.6 dies.
algos = getattr(hashlib,
'algorithms',
('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512'))
return [a for a in algos if a not in set(['md5', 'sha1', 'sha224'])]

def _merge_mapping(option, opt_str, value, parser):
"""Append the value to a list pointed to by the option name in a dict."""
if not parser.values.hashes:
parser.values.hashes = {}
parser.values.hashes.setdefault(opt_str[2:], []).append(value)

def hash_options():
"""Return an iterable of options named after hashlib's algorithms.
Leave out ones weaker than sha256.
"""
for algo_name in _good_hashes():
yield partial(Option,
'--' + algo_name,
# Hash values eventually end up in
# InstallRequirement.hashes due to __dict__ copying in
# process_line().
dest='hashes',
action='callback',
callback=_merge_mapping,
type='string',
help="Verify that the package's archive matches this "
'hash before installing.')


##########
# groups #
Expand Down
2 changes: 1 addition & 1 deletion pip/req/req_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
SUPPORTED_OPTIONS_REQ = [
cmdoptions.install_options,
cmdoptions.global_options
]
] + list(cmdoptions.hash_options())

# the 'dest' string values
SUPPORTED_OPTIONS_REQ_DEST = [o().dest for o in SUPPORTED_OPTIONS_REQ]
Expand Down
18 changes: 18 additions & 0 deletions tests/unit/test_req_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,24 @@ def test_options_on_a_requirement_line(self):
'global_options': ['yo3', 'yo4'],
'install_options': ['yo1', 'yo2']}

def test_hash_options(self):
"""Test the runtime-generated Options that correspond to hashlib
algorithms.
Make sure they read and preserve multiple hashes.
"""
line = ('SomeProject '
'--sha256=2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824 '
'--sha384=59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f '
'--sha256=486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7')
filename = 'filename'
req = list(process_line(line, filename, 1))[0]
assert req.options == {'hashes': {
'sha256': ['2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824',
'486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65260e9cb8a7'],
'sha384': ['59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcdb9c666fa90125a3c79f90397bdf5f6a13de828684f']}}

def test_set_isolated(self, options):
line = 'SomeProject'
filename = 'filename'
Expand Down

0 comments on commit 3303be0

Please sign in to comment.