Skip to content

Instantly share code, notes, and snippets.

@adamghill
Last active September 17, 2025 12:13
Show Gist options
  • Save adamghill/c7b8527068385c42d164efbddf157c0e to your computer and use it in GitHub Desktop.
Save adamghill/c7b8527068385c42d164efbddf157c0e to your computer and use it in GitHub Desktop.
Get Python versions for testing based on `project.requires-python` in pyproject.toml
# Run tests across all supported Python versions
test-all-versions:
#!/usr/bin/env bash
set -euo pipefail
for version in $(yq '.project.requires-python' pyproject.toml | uv run versionator.py); do
echo -e "\n=== Testing with Python $version ==="
uv run --all-extras --python "$version" pytest
echo -e "\n=== Tests completed for Python $version ===\n"
done
echo "All tests completed successfully"
# /// script
# dependencies = [
# "packaging",
# "py-eol",
# ]
# ///
# flake8: noqa: T201
import argparse
import json
import sys
from packaging.requirements import Requirement
from packaging.version import Version
from packaging.version import parse as version_parse
from py_eol import supported_versions
def get_versions(python_specifier: str, is_verbose: bool = False) -> list[str]: # noqa: FBT001, FBT002
current_supported_versions = set(supported_versions())
requirement = Requirement(python_specifier)
versions = set()
minor_versions = set()
"""
Add versions for the floor and ceilings of major.minor versions of the specifier.
Examples:
- if the specifier is "Python>=3.9.7", include 3.9.7 in the `versions`
- if the specifier is "Python<=3.9.7", include 3.9.7 in the `versions`
- if the specifier is "Python<3.9.7", include 3.9.6 in the `versions`
- if the specifier is "Python>3.9.7", include 3.9.8 in the `versions`
"""
for specifier in requirement.specifier:
version = version_parse(specifier.version)
minor_versions.add(version.minor)
if f"{version.major}.{version.minor}" not in current_supported_versions:
if is_verbose:
print(f"WARNING: {version} is EOL.")
continue
if specifier.operator in ("<=", ">=", "="):
versions.add(version)
elif specifier.operator == "<":
if version.micro > 0:
version = Version(f"{version.major}.{version.minor}.{version.micro - 1}")
versions.add(version)
elif specifier.operator == ">":
version = Version(f"{version.major}.{version.minor}.{version.micro + 1}")
versions.add(version)
# Add versions of Python that are currently supported and fit within the specifiers
for supported_version in current_supported_versions:
version = version_parse(supported_version)
if requirement.specifier.contains(version):
if version.minor not in minor_versions:
versions.add(version)
versions = sorted([str(version) for version in versions])
return versions
def main():
parser = argparse.ArgumentParser(description="Process Python versions.")
parser.add_argument("--json", action="store_true", help="Output as JSON array")
parser.add_argument("--verbose", action="store_true", help="More verbose output")
if not sys.stdin.isatty():
python_specifier = sys.stdin.read().strip()
args = parser.parse_args()
else:
parser.add_argument("python_specifier", help="e.g. 'Python>=3.9.7'")
args = parser.parse_args()
python_specifier = args.python_specifier
if args.verbose:
print("~ versionator ~")
print(f"requires-python: {python_specifier}")
if not python_specifier.startswith("Python"):
python_specifier = f"Python{python_specifier}"
versions = get_versions(python_specifier, is_verbose=args.verbose)
if args.verbose:
print()
print("Versions that fit requirements:")
if args.json:
print(json.dumps(versions))
else:
for version in versions:
print(version)
if __name__ == "__main__":
main()
@adamghill
Copy link
Author

adamghill commented Sep 16, 2025

Run with something like yq '.project.requires-python' pyproject.toml | uv run versionator.py.

@adamghill
Copy link
Author

Ended up making this into an actual package: https://pypi.org/project/testable-dependables/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment