Skip to content

Commit 50467e6

Browse files
pip_audit, test: handle invalid requires-python specifiers (#447)
* pip_audit, test: handle invalid requires-python specifiers We follow `pip`'s lead here and ignore these entirely, treating them as if they don't exist (while also warning the user). Fixes #445. See: pypa/packaging#645 Signed-off-by: William Woodruff <[email protected]> * CHANGELOG: record changes Signed-off-by: William Woodruff <[email protected]> Signed-off-by: William Woodruff <[email protected]>
1 parent d295a1a commit 50467e6

File tree

3 files changed

+60
-4
lines changed

3 files changed

+60
-4
lines changed

CHANGELOG.md

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ All versions prior to 0.0.9 are untracked.
88

99
## [Unreleased]
1010

11+
### Fixed
12+
13+
* Fixed a crash triggered when a package specifies an invalid version
14+
specifier for its `requires-python` version
15+
([#447](https://github.com/pypa/pip-audit/pull/447))
16+
1117
## [2.4.10]
1218

1319
### Fixed

pip_audit/_dependency_source/resolvelib/pypi_provider.py

+17-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import annotations
99

1010
import itertools
11+
import logging
1112
from email.message import EmailMessage, Message
1213
from email.parser import BytesParser
1314
from io import BytesIO
@@ -23,7 +24,7 @@
2324
import requests
2425
from cachecontrol import CacheControl
2526
from packaging.requirements import Requirement
26-
from packaging.specifiers import SpecifierSet
27+
from packaging.specifiers import InvalidSpecifier, SpecifierSet
2728
from packaging.utils import canonicalize_name, parse_sdist_filename, parse_wheel_filename
2829
from packaging.version import Version
2930
from resolvelib.providers import AbstractProvider
@@ -34,6 +35,8 @@
3435
from pip_audit._util import python_version
3536
from pip_audit._virtual_env import VirtualEnv, VirtualEnvError
3637

38+
logger = logging.getLogger(__name__)
39+
3740
# TODO: Final[Version] when our minimal Python is 3.8.
3841
PYTHON_VERSION: Version = python_version()
3942

@@ -246,9 +249,19 @@ def get_project_from_index(
246249
py_req = i.attrib.get("data-requires-python")
247250
# Skip items that need a different Python version
248251
if py_req:
249-
spec = SpecifierSet(py_req)
250-
if PYTHON_VERSION not in spec:
251-
continue
252+
try:
253+
# NOTE: Starting with packaging==22.0, specifier parsing is
254+
# stricter: specifier components can only use the wildcard
255+
# comparison syntax on exact comparison operators (== and !=),
256+
# not on ordered operators like `>=`. There are existing
257+
# packages that use the invalid syntax in their metadata
258+
# however (like nltk==3.6, which does requires-python >= 3.5.*),
259+
# so we follow pip`'s behavior and ignore these specifiers.
260+
spec = SpecifierSet(py_req)
261+
if PYTHON_VERSION not in spec:
262+
continue
263+
except InvalidSpecifier:
264+
logger.warning(f"invalid specifier set for Python version: {py_req}")
252265

253266
path = parsed_dist_url.path
254267
filename = path.rpartition("/")[-1]

test/dependency_source/resolvelib/test_resolvelib.py

+37
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from email.message import EmailMessage
44

5+
import pretend
56
import pytest
67
import requests
78
from packaging.requirements import Requirement
@@ -221,6 +222,42 @@ def test_resolvelib_wheel_python_version(monkeypatch):
221222
dict(resolver.resolve_all(iter([req])))
222223

223224

225+
def test_resolvelib_wheel_python_version_invalid_specifier(monkeypatch):
226+
# requires-python is meant to be a valid specifier version, but earlier
227+
# versions of packaging allowed LegacyVersion parsing for invalid versions.
228+
# This changed in packaging==22.0, so we follow pip's lead and ignore
229+
# any Python version specifiers that aren't valid.
230+
# Note that we intentionally test that version that *should* be skipped
231+
# with a valid specifier (<=3.5.*) is instead included.
232+
data = (
233+
'<a href="https://files.pythonhosted.org/packages/54/4f/'
234+
"1b294c1a4ab7b2ad5ca5fc4a9a65a22ef1ac48be126289d97668852d4ab3/Flask-2.0.1-py3-none-any.whl#"
235+
'sha256=a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" '
236+
'data-requires-python="&lt;=3.5.*">Flask-2.0.1-py3-none-any.whl</a><br/>'
237+
)
238+
239+
logger = pretend.stub(warning=pretend.call_recorder(lambda s: None))
240+
monkeypatch.setattr(pypi_provider, "logger", logger)
241+
242+
monkeypatch.setattr(
243+
pypi_provider.Candidate, "_get_metadata_for_wheel", lambda _: get_metadata_mock()
244+
)
245+
246+
resolver = resolvelib.ResolveLibResolver()
247+
monkeypatch.setattr(
248+
resolver.provider.session, "get", lambda _url, **kwargs: get_package_mock(data)
249+
)
250+
251+
req = Requirement("flask==2.0.1")
252+
resolved_deps = dict(resolver.resolve_all(iter([req])))
253+
assert req in resolved_deps
254+
assert resolved_deps[req] == [ResolvedDependency("flask", Version("2.0.1"))]
255+
256+
assert logger.warning.calls == [
257+
pretend.call("invalid specifier set for Python version: <=3.5.*")
258+
]
259+
260+
224261
def test_resolvelib_wheel_canonical_name_mismatch(monkeypatch):
225262
# Call the underlying wheel, Mask instead of Flask. This should throw an `ResolutionImpossible`
226263
# error.

0 commit comments

Comments
 (0)