diff --git a/packages.ini b/packages.ini index 7444419e..444260bc 100644 --- a/packages.ini +++ b/packages.ini @@ -190,6 +190,7 @@ python_versions = <3.12 [certifi==2025.6.15] [certifi==2025.7.14] [certifi==2025.8.3] +[certifi==2025.10.5] [cffi==1.14.6] apt_requires = libffi-dev @@ -235,6 +236,7 @@ python_versions = <3.13 [charset-normalizer==3.4.1] [charset-normalizer==3.4.2] [charset-normalizer==3.4.3] +[charset-normalizer==3.4.4] [click==7.1.2] [click==8.0.4] @@ -546,6 +548,7 @@ validate_incorrect_missing_deps = psycopg2-binary [docutils==0.18.1] [docutils==0.19] +[docutils==0.22.3] [drf-spectacular==0.22.1] [drf-spectacular==0.23.1] @@ -895,6 +898,8 @@ python_versions = <3.12 [hypothesis==6.61.0] validate_extras = pytest +[id==1.5.0] + [identify==2.5.1] [identify==2.5.3] [identify==2.5.5] @@ -914,6 +919,7 @@ validate_extras = pytest [idna==3.7] [idna==3.8] [idna==3.10] +[idna==3.11] [imagesize==1.4.1] @@ -948,6 +954,15 @@ python_versions = <3.12 [itsdangerous==2.1.2] [itsdangerous==2.2.0] +[jaraco-classes==3.4.0] +python_versions = >=3.12 + +[jaraco-context==6.0.1] +python_versions = >=3.12 + +[jaraco-functools==4.3.0] +python_versions = >=3.12 + [jinja2==3.0.3] [jinja2==3.1.0] [jinja2==3.1.2] @@ -997,6 +1012,8 @@ python_versions = <3.13 [jsonschema2md==0.4.0] +[keyring==25.6.0] + [kombu==4.6.11] [kombu==5.2.4] [kombu==5.3.3] @@ -1122,6 +1139,7 @@ brew_requires = libmaxminddb [more-itertools==9.0.0] [more-itertools==10.0.0] [more-itertools==10.1.0] +[more-itertools==10.8.0] [msgpack==1.0.4] python_versions = <3.12 @@ -1185,6 +1203,8 @@ python_versions = <3.13 [natsort==8.1.0] [natsort==8.2.0] +[nh3==0.3.2] + [nodeenv==1.6.0] [nodeenv==1.7.0] [nodeenv==1.8.0] @@ -1748,6 +1768,8 @@ python_versions = <3.12 [rb==1.9.0] [rb==1.10.0] +[readme-renderer==44.0] + [redis==3.4.1] [redis==3.5.3] [redis==4.3.4] @@ -1793,6 +1815,8 @@ python_versions = <3.12 [requests-oauthlib==1.3.1] [requests-oauthlib==2.0.0] +[requests-toolbelt==1.0.0] + [responses==0.21.0] [responses==0.22.0] [responses==0.23.1] @@ -1812,11 +1836,13 @@ python_versions = <3.12 [rfc3339-validator==0.1.4] [rfc3986==1.5.0] +[rfc3986==2.0.0] [rfc3986-validator==0.1.1] [rich==13.8.1] [rich==14.1.0] +[rich==14.2.0] [rpds-py==0.9.2] python_versions = <3.13 @@ -2881,6 +2907,7 @@ python_versions = >=3.10 [setuptools==74.1.2] [setuptools==78.1.0] [setuptools==78.1.1] +[setuptools==80.9.0] [setuptools-rust==1.5.2] @@ -3128,6 +3155,8 @@ python_versions = <3.13 [trio-websocket==0.11.1] [trio-websocket==0.12.2] +[twine==6.1.0] + [types-beautifulsoup4==4.11.6] [types-beautifulsoup4==4.11.6.7] [types-beautifulsoup4==4.12.0.6] diff --git a/tests/validate_test.py b/tests/validate_test.py index 6484069d..1dd2d803 100644 --- a/tests/validate_test.py +++ b/tests/validate_test.py @@ -3,6 +3,7 @@ import zipfile import pytest +from packaging.specifiers import SpecifierSet from packaging.tags import parse_tag import validate @@ -97,3 +98,10 @@ def test_top_imports_record(tmp_path): expected = ["distlib", "_distlib_backend", "distlib_top"] assert validate._top_imports(str(whl)) == expected + + +def test_pythons_to_check_with_python_versions_constraint(): + tag = parse_tag("py2.py3-none-any") + constraint = SpecifierSet(">=3.12") + ret = validate._pythons_to_check(tag, constraint) + assert ret == ("python3.12", "python3.13") diff --git a/validate.py b/validate.py index a04acb01..0cf02f3b 100644 --- a/validate.py +++ b/validate.py @@ -12,10 +12,13 @@ from typing import NamedTuple import packaging.tags +from packaging.specifiers import SpecifierSet from packaging.tags import Tag from packaging.utils import parse_wheel_filename from packaging.version import Version +from build import Package + PYTHONS = ((3, 11), (3, 12), (3, 13)) DIST_INFO_RE = re.compile(r"^[^/]+.dist-info/[^/]+$") @@ -44,27 +47,47 @@ def _py_exe(major: int, minor: int) -> str: return f"python{major}.{minor}" -def _pythons_to_check(tags: frozenset[Tag]) -> tuple[str, ...]: - ret: set[str] = set() +def _pythons_to_check( + tags: frozenset[Tag], python_versions: SpecifierSet | None = None +) -> tuple[str, ...]: + tag_compatible_pythons: set[str] = set() for tag in tags: if tag.abi == "abi3" and tag.interpreter.startswith("cp"): min_py = _parse_cp_tag(tag.interpreter) - ret.update(_py_exe(*py) for py in PYTHONS if py >= min_py) + tag_compatible_pythons.update( + _py_exe(*py) for py in PYTHONS if py >= min_py + ) elif tag.interpreter.startswith("cp"): - ret.add(_py_exe(*_parse_cp_tag(tag.interpreter))) + tag_compatible_pythons.add(_py_exe(*_parse_cp_tag(tag.interpreter))) elif tag.interpreter == "py2": continue elif tag.interpreter.startswith("py3"): for py in PYTHONS: - if tag in packaging.tags.compatible_tags(py): - ret.add(_py_exe(*py)) + if tag not in packaging.tags.compatible_tags(py): + raise AssertionError(f"{tag} is not compatible with python {py}") + tag_compatible_pythons.add(_py_exe(*py)) else: raise AssertionError(f"unexpected tag: {tag}") - if not ret: + if not tag_compatible_pythons: raise AssertionError(f"no interpreters found for {tags}") - else: - return tuple(sorted(ret)) + + if not python_versions: + return tuple(sorted(tag_compatible_pythons)) + + package_compatible_pythons: set[str] = set() + for python_exe in tag_compatible_pythons: + py_version_str = python_exe.replace("python", "") + if py_version_str in python_versions: + package_compatible_pythons.add(python_exe) + + if not package_compatible_pythons: + raise AssertionError( + f"no interpreters found for {tags} after applying package constraints {python_versions}. " + f"Wheel supports: {sorted(tag_compatible_pythons)}" + ) + + return tuple(sorted(package_compatible_pythons)) def _top_imports(whl: str) -> list[str]: @@ -156,14 +179,18 @@ def main() -> int: raise SystemExit(f"{args.packages_ini}: not found") packages = {} + validate_infos = {} for k in cfg.sections(): pkg, _, version_s = k.partition("==") - packages[(pkg, Version(version_s))] = Info.from_dct(cfg[k]) + key = (pkg, Version(version_s)) + packages[key] = Package.make(k, cfg[k]) + validate_infos[key] = Info.from_dct(cfg[k]) for filename in sorted(os.listdir(args.dist)): name, version, _, wheel_tags = parse_wheel_filename(filename) - info = packages[(name, version)] - for python in _pythons_to_check(wheel_tags): + package = packages[(name, version)] + info = validate_infos[(name, version)] + for python in _pythons_to_check(wheel_tags, package.python_versions): _validate( python=python, filename=os.path.join(args.dist, filename),