Repository: trailofbits/pip-audit (python)

Issue:
pip-audit crashes with unusual Python versions
does #139 also address python itself having a version like `3.9.7+`? this is what development python versions like `3.9-dev` built via `pyenv` end up with – not sure if this is python's or pyenv's doing.

the stack trace looks different:

```
$ pip-audit
Traceback (most recent call last):
  File "bin/pip-audit", line 5, in <module>
    from pip_audit._cli import audit
  File "lib/python3.9/site-packages/pip_audit/_cli.py", line 15, in <module>
    from pip_audit._audit import AuditOptions, Auditor
  File "lib/python3.9/site-packages/pip_audit/_audit.py", line 9, in <module>
    from pip_audit._dependency_source import DependencySource
  File "lib/python3.9/site-packages/pip_audit/_dependency_source/__init__.py", line 13, in <module>
    from .resolvelib import ResolveLibResolver  # noqa: F401
  File "lib/python3.9/site-packages/pip_audit/_dependency_source/resolvelib/__init__.py", line 5, in <module>
    from .resolvelib import ResolveLibResolver, ResolveLibResolverError  # noqa: F401
  File "lib/python3.9/site-packages/pip_audit/_dependency_source/resolvelib/resolvelib.py", line 16, in <module>
    from .pypi_provider import PyPIProvider
  File "lib/python3.9/site-packages/pip_audit/_dependency_source/resolvelib/pypi_provider.py", line 31, in <module>
    PYTHON_VERSION = Version(python_version())
  File "lib/python3.9/site-packages/packaging/version.py", line 266, in __init__
    raise InvalidVersion(f"Invalid version: '{version}'")
packaging.version.InvalidVersion: Invalid version: '3.9.7+'
```

_Originally posted by @wbolster in https://github.com/trailofbits/pip-audit/issues/138#issuecomment-984603768_

Pull request:
pip_audit, test: make Python runtime version retrieval more resilient
Closes #164.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2201c14..6bafbfb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -28,6 +28,10 @@ All versions prior to 0.0.9 are untracked.
 
 ### Fixed
 
+* Dependency sources: a crash caused by `platform.python_version` returning
+  an version string that couldn't be parsed as a PEP-440 version was fixed
+  ([#175](https://github.com/trailofbits/pip-audit/pull/175))
+
 ### Removed
 
 ## [1.0.1] - 2021-12-02
diff --git a/pip_audit/_dependency_source/resolvelib/pypi_provider.py b/pip_audit/_dependency_source/resolvelib/pypi_provider.py
index b4e73a6..3228ce3 100644
--- a/pip_audit/_dependency_source/resolvelib/pypi_provider.py
+++ b/pip_audit/_dependency_source/resolvelib/pypi_provider.py
@@ -10,7 +10,6 @@ from email.message import EmailMessage
 from email.parser import BytesParser
 from io import BytesIO
 from operator import attrgetter
-from platform import python_version
 from tarfile import TarFile
 from tempfile import TemporaryDirectory
 from typing import BinaryIO, List, Optional, Set, cast
@@ -26,9 +25,11 @@ from packaging.version import Version
 from resolvelib.providers import AbstractProvider
 
 from pip_audit._state import AuditState
+from pip_audit._util import python_version
 from pip_audit._virtual_env import VirtualEnv
 
-PYTHON_VERSION = Version(python_version())
+# TODO: Final[Version] when our minimal Python is 3.8.
+PYTHON_VERSION: Version = python_version()
 
 
 class Candidate:
diff --git a/pip_audit/_util.py b/pip_audit/_util.py
index fbb272e..e06fe9c 100644
--- a/pip_audit/_util.py
+++ b/pip_audit/_util.py
@@ -2,11 +2,25 @@
 Utilities functions for `pip-audit`.
 """
 
+import sys
 from typing import NoReturn  # pragma: no cover
 
+from packaging.version import Version
+
 
 def assert_never(x: NoReturn) -> NoReturn:  # pragma: no cover
     """
     A hint to the typechecker that a branch can never occur.
     """
     assert False, f"unhandled type: {type(x).__name__}"
+
+
+def python_version() -> Version:
+    """
+    Return a PEP-440-style version for the current Python interpreter.
+
+    This is more rigorous than `platform.python_version`, which can include
+    non-PEP-440-compatible data.
+    """
+    info = sys.version_info
+    return Version(f"{info.major}.{info.minor}.{info.micro}")


Test patch:
diff --git a/test/test_util.py b/test/test_util.py
new file mode 100644
index 0000000..86a7452
--- /dev/null
+++ b/test/test_util.py
@@ -0,0 +1,9 @@
+from packaging.version import Version
+
+import pip_audit._util as util
+
+
+def test_python_version():
+    v = util.python_version()
+    assert v is not None
+    assert isinstance(v, Version)