cpython/Doc/tools/extensions/patchlevel.py

"""Extract version information from Include/patchlevel.h."""

import re
import sys
from pathlib import Path
from typing import Literal, NamedTuple

CPYTHON_ROOT = Path(
    __file__,  # cpython/Doc/tools/extensions/patchlevel.py
    "..",  # cpython/Doc/tools/extensions
    "..",  # cpython/Doc/tools
    "..",  # cpython/Doc
    "..",  # cpython
).resolve()
PATCHLEVEL_H = CPYTHON_ROOT / "Include" / "patchlevel.h"

RELEASE_LEVELS = {
    "PY_RELEASE_LEVEL_ALPHA": "alpha",
    "PY_RELEASE_LEVEL_BETA": "beta",
    "PY_RELEASE_LEVEL_GAMMA": "candidate",
    "PY_RELEASE_LEVEL_FINAL": "final",
}


class version_info(NamedTuple):  # noqa: N801
    major: int  #: Major release number
    minor: int  #: Minor release number
    micro: int  #: Patch release number
    releaselevel: Literal["alpha", "beta", "candidate", "final"]
    serial: int  #: Serial release number


def get_header_version_info() -> version_info:
    # Capture PY_ prefixed #defines.
    pat = re.compile(r"\s*#define\s+(PY_\w*)\s+(\w+)", re.ASCII)

    defines = {}
    patchlevel_h = PATCHLEVEL_H.read_text(encoding="utf-8")
    for line in patchlevel_h.splitlines():
        if (m := pat.match(line)) is not None:
            name, value = m.groups()
            defines[name] = value

    return version_info(
        major=int(defines["PY_MAJOR_VERSION"]),
        minor=int(defines["PY_MINOR_VERSION"]),
        micro=int(defines["PY_MICRO_VERSION"]),
        releaselevel=RELEASE_LEVELS[defines["PY_RELEASE_LEVEL"]],
        serial=int(defines["PY_RELEASE_SERIAL"]),
    )


def format_version_info(info: version_info) -> tuple[str, str]:
    version = f"{info.major}.{info.minor}"
    release = f"{info.major}.{info.minor}.{info.micro}"
    if info.releaselevel != "final":
        suffix = {"alpha": "a", "beta": "b", "candidate": "rc"}
        release += f"{suffix[info.releaselevel]}{info.serial}"
    return version, release


def get_version_info():
    try:
        info = get_header_version_info()
        return format_version_info(info)
    except OSError:
        version, release = format_version_info(sys.version_info)
        print(
            f"Failed to get version info from Include/patchlevel.h, "
            f"using version of this interpreter ({release}).",
            file=sys.stderr,
        )
        return version, release


if __name__ == "__main__":
    print(format_version_info(get_header_version_info())[1])