py-spy 0.4.2

Sampling profiler for Python programs
Documentation
import argparse
from collections import defaultdict
import requests
import pathlib
import yaml
import re

_VERSIONS_URL = "https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json"  # noqa

_OSX_PYTHON_EXCLUSIONS = []


def parse_version(v):
    return tuple(int(part) for part in re.split(r"\W", v)[:3])


def get_github_python_versions():
    versions_json = requests.get(_VERSIONS_URL).json()

    # windows platform support isn't great for older versions of python
    # get a map of version: platform/arch so we can exclude here
    platforms = {}
    for v in versions_json:
        version_platforms = set()
        for f in v["files"]:
            platform, arch = f["platform"], f["arch"]
            if platform == "linux" and f.get("platform_version") != "22.04":
                continue
            version_platforms.add((platform, arch))
        platforms[v["version"]] = version_platforms

    raw_versions = [v["version"] for v in versions_json]
    minor_versions = defaultdict(list)

    for version_str in raw_versions:
        if "-" in version_str:
            continue

        major, minor, patch = parse_version(version_str)
        if major == 3 and minor < 6:
            # we don't support python 3.0/3.1/3.2 , and don't bother testing 3.3/3.4/3.5
            continue

        elif major == 2 and minor < 7:
            # we don't test python support before 2.7
            continue
        minor_versions[(major, minor)].append(patch)

    versions = []
    for (major, minor), patches in minor_versions.items():
        patches.sort()

        # for older versions of python, don't test all patches
        # (just test first and last) to keep the test matrix down
        if major == 2 or minor <= 12:
            patches = [patches[0], patches[-1]]

        if major == 3 and minor > 14:
            continue

        versions.extend(f"{major}.{minor}.{patch}" for patch in patches)

    return versions, platforms


def update_python_test_versions(force=False):
    versions, platforms = get_github_python_versions()
    versions = sorted(versions, key=parse_version)

    build_yml_path = (
        pathlib.Path(__file__).parent.parent / ".github" / "workflows" / "build.yml"
    )

    build_yml = yaml.safe_load(open(build_yml_path))
    test_matrix = build_yml["jobs"]["test-wheels"]["strategy"]["matrix"]
    existing_python_versions = test_matrix["python-version"]
    if not force and versions == existing_python_versions:
        print("No new python versions found - not updating github actions")
        return

    print("Adding new versions")
    print("Old:", existing_python_versions)
    print("New:", versions)

    # we can't use the yaml package to update the GHA script, since
    # the data in build_yml is treated as an unordered dictionary.
    # instead modify the file in place
    lines = list(open(build_yml_path))
    first_line = lines.index(
        "      # automatically generated by ci/update_python_test_versions.py\n"
    )

    first_version_line = lines.index("          [\n", first_line)
    last_version_line = lines.index("          ]\n", first_version_line)
    new_versions = [f"            {v},\n" for v in versions]
    lines = lines[: first_version_line + 1] + new_versions + lines[last_version_line:]

    # also automatically exclude >= v3.11.* from running on OSX,
    # since it currently fails in GHA on SIP errors
    exclusions = []
    for v in versions:
        # if we don't have a python version for the platform, skip it in GHA
        # also, ignore python 3.12.* on OSX
        if ("darwin", "arm64") not in platforms[v] or any(
            v.startswith(pattern) for pattern in _OSX_PYTHON_EXCLUSIONS
        ):
            exclusions.append("          - os: macos-latest\n")
            exclusions.append(f"            python-version: {v}\n")

        if ("win32", "x64") not in platforms[v]:
            exclusions.append("          - os: windows-latest\n")
            exclusions.append(f"            python-version: {v}\n")

        if ("linux", "x64") not in platforms[v]:
            exclusions.append("          - os: ubuntu-22.04\n")
            exclusions.append(f"            python-version: {v}\n")

        if ("linux", "arm64") not in platforms[v]:
            exclusions.append("          - os: ubuntu-22.04-arm\n")
            exclusions.append(f"            python-version: {v}\n")

    first_exclude_line = lines.index("        exclude:\n", first_line)
    last_exclude_line = lines.index("\n", first_exclude_line)
    lines = lines[: first_exclude_line + 1] + exclusions + lines[last_exclude_line:]

    with open(build_yml_path, "w") as o:
        o.write("".join(lines))


if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Updates github actions with new python versions",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument(
        "--force",
        help="Run script even if there are no new python versions",
        action="store_true",
    )
    args = parser.parse_args()

    update_python_test_versions(force=args.force)