Repository: peterbe/hashin

Issue:
`hashin black` fails
```
▶ hashin black
Traceback (most recent call last):
  File "/usr/local/bin/hashin", line 11, in <module>
    sys.exit(main())
  File "/usr/local/lib/python3.6/site-packages/hashin.py", line 474, in main
    verbose=args.verbose,
  File "/usr/local/lib/python3.6/site-packages/hashin.py", line 120, in run
    run_single_package(spec, *args, **kwargs)
  File "/usr/local/lib/python3.6/site-packages/hashin.py", line 146, in run_single_package
    algorithm=algorithm
  File "/usr/local/lib/python3.6/site-packages/hashin.py", line 407, in get_package_hashes
    version = get_latest_version(data)
  File "/usr/local/lib/python3.6/site-packages/hashin.py", line 223, in get_latest_version
    return str(all_versions[0][1])
IndexError: list index out of range
```


Discussion:

Patch:
diff --git a/hashin.py b/hashin.py
index c1bb79b..1590560 100755
--- a/hashin.py
+++ b/hashin.py
@@ -58,6 +58,11 @@ parser.add_argument(
     help='Verbose output',
     action='store_true',
 )
+parser.add_argument(
+    '--include-prereleases',
+    help='Include pre-releases (off by default)',
+    action='store_true',
+)
 parser.add_argument(
     '-p', '--python-version',
     help='Python version to add wheels for. May be used multiple times.',
@@ -83,6 +88,10 @@ class PackageError(Exception):
     pass
 
 
+class NoVersionsError(Exception):
+    """When there are no valid versions found."""
+
+
 def _verbose(*args):
     print('* ' + ' '.join(args))
 
@@ -127,6 +136,7 @@ def run_single_package(
     algorithm,
     python_versions=None,
     verbose=False,
+    include_prereleases=False,
 ):
     restriction = None
     if ';' in spec:
@@ -143,7 +153,8 @@ def run_single_package(
         version=version,
         verbose=verbose,
         python_versions=python_versions,
-        algorithm=algorithm
+        algorithm=algorithm,
+        include_prereleases=include_prereleases,
     )
     package = data['package']
 
@@ -202,7 +213,7 @@ def amend_requirements_content(requirements, package, new_lines):
     return requirements
 
 
-def get_latest_version(data):
+def get_latest_version(data, include_prereleases):
     """
     Return the version string of what we think is the latest version.
     In the data blob from PyPI there is the info->version key which
@@ -214,11 +225,22 @@ def get_latest_version(data):
         # This feels kinda strange but it has worked for years
         return data['info']['version']
     all_versions = []
+    count_prereleases = 0
     for version in data['releases']:
         v = parse(version)
-        if not v.is_prerelease:
+        if not v.is_prerelease or include_prereleases:
             all_versions.append((v, version))
+        else:
+            count_prereleases += 1
     all_versions.sort(reverse=True)
+    if not all_versions:
+        msg = "Not a single valid version found."
+        if not include_prereleases and count_prereleases:
+            msg += (
+                " But, found {0} pre-releases. Consider running again "
+                "with the --include-prereleases flag."
+            )
+        raise NoVersionsError(msg)
     # return the highest non-pre-release version
     return str(all_versions[0][1])
 
@@ -378,6 +400,7 @@ def get_package_hashes(
     algorithm=DEFAULT_ALGORITHM,
     python_versions=(),
     verbose=False,
+    include_prereleases=False,
 ):
     """
     Gets the hashes for the given package.
@@ -404,7 +427,7 @@ def get_package_hashes(
     """
     data = get_package_data(package, verbose)
     if not version:
-        version = get_latest_version(data)
+        version = get_latest_version(data, include_prereleases)
         assert version
         if verbose:
             _verbose('Latest version for {0} is {1}'.format(
@@ -472,6 +495,7 @@ def main():
             args.algorithm,
             args.python_version,
             verbose=args.verbose,
+            include_prereleases=args.include_prereleases,
         )
     except PackageError as exception:
         print(str(exception), file=sys.stderr)


Test patch:
diff --git a/tests/test_arg_parse.py b/tests/test_arg_parse.py
index 7d00e74..a6b6236 100644
--- a/tests/test_arg_parse.py
+++ b/tests/test_arg_parse.py
@@ -18,6 +18,7 @@ def test_everything():
         requirements_file='reqs.txt',
         verbose=True,
         version=False,
+        include_prereleases=False,
     )
     assert args == (expected, [])
 
@@ -37,6 +38,7 @@ def test_everything_long():
         requirements_file='reqs.txt',
         verbose=True,
         version=False,
+        include_prereleases=False,
     )
     assert args == (expected, [])
 
@@ -50,5 +52,6 @@ def test_minimal():
         requirements_file='requirements.txt',
         verbose=False,
         version=False,
+        include_prereleases=False,
     )
     assert args == (expected, [])
diff --git a/tests/test_cli.py b/tests/test_cli.py
index c0b566f..72cf2de 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -73,7 +73,10 @@ class Tests(TestCase):
 
     @mock.patch('hashin.urlopen')
     def test_get_latest_version_simple(self, murlopen):
-        version = hashin.get_latest_version({'info': {'version': '0.3'}})
+        version = hashin.get_latest_version(
+            {'info': {'version': '0.3'}},
+            False
+        )
         self.assertEqual(version, '0.3')
 
     @mock.patch('hashin.urlopen')
@@ -91,9 +94,43 @@ class Tests(TestCase):
                 '2.0b2': {},
                 '2.0c3': {},
             }
-        })
+        }, False)
         self.assertEqual(version, '0.999')
 
+    @mock.patch('hashin.urlopen')
+    def test_get_latest_version_only_pre_release(self, murlopen):
+        self.assertRaises(
+            hashin.NoVersionsError,
+            hashin.get_latest_version,
+            {
+                'info': {
+                    'version': '0.3',
+                },
+                'releases': {
+                    '1.1.0rc1': {},
+                    '1.1rc1': {},
+                    '1.0a1': {},
+                    '2.0b2': {},
+                    '2.0c3': {},
+                }
+            },
+            False,
+        )
+
+        version = hashin.get_latest_version({
+            'info': {
+                'version': '0.3',
+            },
+            'releases': {
+                '1.1.0rc1': {},
+                '1.1rc1': {},
+                '1.0a1': {},
+                '2.0b2': {},
+                '2.0c3': {},
+            }
+        }, True)
+        self.assertEqual(version, '2.0c3')
+
     @mock.patch('hashin.urlopen')
     def test_get_latest_version_non_pre_release_leading_zeros(self, murlopen):
         version = hashin.get_latest_version({
@@ -105,7 +142,7 @@ class Tests(TestCase):
                 '0.04.21': {},
                 '0.04.09': {},
             }
-        })
+        }, False)
         self.assertEqual(version, '0.04.21')
 
     @mock.patch('hashin.urlopen')