diff --git a/README.rst b/README.rst index 40a8007..9c29ac9 100644 --- a/README.rst +++ b/README.rst @@ -121,6 +121,26 @@ these exact identifiers directly, if you need something specific. The ``source`` release is always automatically included. ``pip`` will use this as a fallback in the case a suitable wheel cannot be found. +Dry run mode +============ + +There are some use cases, when you maybe don't want to edit your ``requirements.txt`` +right away. You can use the ``--dry-run`` argument to show the diff, so you +can preview the changes to your ``requirements.txt`` file. + +Example:: + + hashin --dry-run requests==2.19.1 + +Would result in a printout on the command line:: + + --- Old + +++ New + @@ -0,0 +1,3 @@ + +requests==2.19.1 \ + + --hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \ + + --hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a + PEP-0496 Environment Markers ============================ diff --git a/hashin.py b/hashin.py index 47fb6ab..28b8083 100755 --- a/hashin.py +++ b/hashin.py @@ -15,6 +15,7 @@ import pip_api from packaging.version import parse +import difflib if sys.version_info >= (3,): from urllib.request import urlopen @@ -75,6 +76,12 @@ action='store_true', default=False, ) +parser.add_argument( + '--dry-run', + help="Don't touch requirements.txt and just show the diff", + action='store_true', + default=False, +) major_pip_version = int(pip_api.version().split('.')[0]) @@ -101,7 +108,7 @@ def _download(url, binary=False): # Note that urlopen will, by default, follow redirects. status_code = r.getcode() - if status_code >= 301 and status_code < 400: + if 301 <= status_code < 400: location, _ = cgi.parse_header(r.headers.get('location', '')) if not location: raise PackageError("No 'Location' header on {0} ({1})".format( @@ -137,6 +144,7 @@ def run_single_package( python_versions=None, verbose=False, include_prereleases=False, + dry_run=False, ): restriction = None if ';' in spec: @@ -159,7 +167,6 @@ def run_single_package( package = data['package'] maybe_restriction = '' if not restriction else '; {0}'.format(restriction) - new_lines = '' new_lines = '{0}=={1}{2} \\\n'.format( package, data['version'], @@ -175,17 +182,31 @@ def run_single_package( new_lines += ' \\' new_lines += '\n' - if verbose: - _verbose('Editing', file) with open(file) as f: - requirements = f.read() + old_requirements = f.read() requirements = amend_requirements_content( - requirements, + old_requirements, package, new_lines ) - with open(file, 'w') as f: - f.write(requirements) + if dry_run: + if verbose: + _verbose('Dry run, not editing ', file) + print( + "".join( + difflib.unified_diff( + old_requirements.splitlines(True), + requirements.splitlines(True), + fromfile="Old", + tofile="New", + ) + ) + ) + else: + with open(file, 'w') as f: + f.write(requirements) + if verbose: + _verbose('Editing', file) def amend_requirements_content(requirements, package, new_lines): @@ -498,6 +519,7 @@ def main(): args.python_version, verbose=args.verbose, include_prereleases=args.include_prereleases, + dry_run=args.dry_run, ) except PackageError as exception: print(str(exception), file=sys.stderr) diff --git a/tests/test_arg_parse.py b/tests/test_arg_parse.py index a6b6236..e38eb88 100644 --- a/tests/test_arg_parse.py +++ b/tests/test_arg_parse.py @@ -9,7 +9,7 @@ def test_everything(): '-r', 'reqs.txt', '-a', 'sha512', '-p', '3.5', - '-v', + '-v', '--dry-run' ]) expected = argparse.Namespace( algorithm='sha512', @@ -19,6 +19,7 @@ def test_everything(): verbose=True, version=False, include_prereleases=False, + dry_run=True, ) assert args == (expected, []) @@ -30,6 +31,7 @@ def test_everything_long(): '--algorithm', 'sha512', '--python-version', '3.5', '--verbose', + '--dry-run', ]) expected = argparse.Namespace( algorithm='sha512', @@ -39,6 +41,7 @@ def test_everything_long(): verbose=True, version=False, include_prereleases=False, + dry_run=True, ) assert args == (expected, []) @@ -53,5 +56,6 @@ def test_minimal(): verbose=False, version=False, include_prereleases=False, + dry_run=False, ) assert args == (expected, []) diff --git a/tests/test_cli.py b/tests/test_cli.py index 48472a1..a536d6b 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -685,6 +685,72 @@ def mocked_get(url, **options): 'hashin==0.11 \\' ) + @cleanup_tmpdir('hashin*') + @mock.patch('hashin.urlopen') + def test_run_dry(self, murlopen): + """dry run should edit the requirements.txt file and print + hashes and package name in the console + """ + + def mocked_get(url, **options): + if url == 'https://pypi.org/pypi/hashin/json': + return _Response({ + 'info': { + 'version': '0.11', + 'name': 'hashin', + }, + 'releases': { + '0.11': [ + { + 'url': 'https://pypi.org/packages/source/p/hashin/hashin-0.11.tar.gz', + 'digests': { + 'sha256': 'bbbbb', + }, + } + ], + '0.10': [ + { + 'url': 'https://pypi.org/packages/source/p/hashin/hashin-0.10.tar.gz', + 'digests': { + 'sha256': 'aaaaa', + }, + } + ] + } + }) + + murlopen.side_effect = mocked_get + + with tmpfile() as filename: + with open(filename, 'w') as f: + f.write('') + + my_stdout = StringIO() + with redirect_stdout(my_stdout): + retcode = hashin.run( + 'hashin==0.10', + filename, + 'sha256', + verbose=False, + dry_run=True, + ) + + self.assertEqual(retcode, 0) + + # verify that nothing has been written to file + with open(filename) as f: + output = f.read() + assert not output + + # Check dry run output + out_lines = my_stdout.getvalue().splitlines() + self.assertTrue( + '+hashin==0.10' in out_lines[3] + ) + self.assertTrue( + '+--hash=sha256:aaaaa' in out_lines[4].replace(" ", "") + ) + @cleanup_tmpdir('hashin*') @mock.patch('hashin.urlopen') def test_run_pep_0496(self, murlopen):