Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pip_audit, test: handle invalid requires-python specifiers #447

Merged
merged 2 commits into from
Dec 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ All versions prior to 0.0.9 are untracked.

## [Unreleased]

### Fixed

* Fixed a crash triggered when a package specifies an invalid version
specifier for its `requires-python` version
([#447](https://github.com/pypa/pip-audit/pull/447))

## [2.4.10]

### Fixed
Expand Down
21 changes: 17 additions & 4 deletions pip_audit/_dependency_source/resolvelib/pypi_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from __future__ import annotations

import itertools
import logging
from email.message import EmailMessage, Message
from email.parser import BytesParser
from io import BytesIO
Expand All @@ -23,7 +24,7 @@
import requests
from cachecontrol import CacheControl
from packaging.requirements import Requirement
from packaging.specifiers import SpecifierSet
from packaging.specifiers import InvalidSpecifier, SpecifierSet
from packaging.utils import canonicalize_name, parse_sdist_filename, parse_wheel_filename
from packaging.version import Version
from resolvelib.providers import AbstractProvider
Expand All @@ -34,6 +35,8 @@
from pip_audit._util import python_version
from pip_audit._virtual_env import VirtualEnv, VirtualEnvError

logger = logging.getLogger(__name__)

# TODO: Final[Version] when our minimal Python is 3.8.
PYTHON_VERSION: Version = python_version()

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

path = parsed_dist_url.path
filename = path.rpartition("/")[-1]
Expand Down
37 changes: 37 additions & 0 deletions test/dependency_source/resolvelib/test_resolvelib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from email.message import EmailMessage

import pretend
import pytest
import requests
from packaging.requirements import Requirement
Expand Down Expand Up @@ -221,6 +222,42 @@ def test_resolvelib_wheel_python_version(monkeypatch):
dict(resolver.resolve_all(iter([req])))


def test_resolvelib_wheel_python_version_invalid_specifier(monkeypatch):
# requires-python is meant to be a valid specifier version, but earlier
# versions of packaging allowed LegacyVersion parsing for invalid versions.
# This changed in packaging==22.0, so we follow pip's lead and ignore
# any Python version specifiers that aren't valid.
# Note that we intentionally test that version that *should* be skipped
# with a valid specifier (<=3.5.*) is instead included.
data = (
'<a href="https://files.pythonhosted.org/packages/54/4f/'
"1b294c1a4ab7b2ad5ca5fc4a9a65a22ef1ac48be126289d97668852d4ab3/Flask-2.0.1-py3-none-any.whl#"
'sha256=a6209ca15eb63fc9385f38e452704113d679511d9574d09b2cf9183ae7d20dc9" '
'data-requires-python="&lt;=3.5.*">Flask-2.0.1-py3-none-any.whl</a><br/>'
)

logger = pretend.stub(warning=pretend.call_recorder(lambda s: None))
monkeypatch.setattr(pypi_provider, "logger", logger)

monkeypatch.setattr(
pypi_provider.Candidate, "_get_metadata_for_wheel", lambda _: get_metadata_mock()
)

resolver = resolvelib.ResolveLibResolver()
monkeypatch.setattr(
resolver.provider.session, "get", lambda _url, **kwargs: get_package_mock(data)
)

req = Requirement("flask==2.0.1")
resolved_deps = dict(resolver.resolve_all(iter([req])))
assert req in resolved_deps
assert resolved_deps[req] == [ResolvedDependency("flask", Version("2.0.1"))]

assert logger.warning.calls == [
pretend.call("invalid specifier set for Python version: <=3.5.*")
]


def test_resolvelib_wheel_canonical_name_mismatch(monkeypatch):
# Call the underlying wheel, Mask instead of Flask. This should throw an `ResolutionImpossible`
# error.
Expand Down