Skip to content

Commit 36ffaf4

Browse files
committed
Add to Tools/build/ instead
1 parent d0e059c commit 36ffaf4

File tree

1 file changed

+160
-0
lines changed

1 file changed

+160
-0
lines changed

Tools/build/patchlevel.py

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
"""Extract version information from Include/patchlevel.h."""
2+
3+
import re
4+
import sys
5+
from pathlib import Path
6+
from typing import Literal, NamedTuple
7+
8+
CPYTHON_ROOT = Path(
9+
__file__, # cpython/Tools/build/patchlevel.py
10+
"..", # cpython/Tools/build
11+
"..", # cpython/Tools
12+
"..", # cpython
13+
).resolve()
14+
PATCHLEVEL_H = CPYTHON_ROOT / "Include" / "patchlevel.h"
15+
16+
RELEASE_LEVELS = {
17+
"PY_RELEASE_LEVEL_ALPHA": "alpha",
18+
"PY_RELEASE_LEVEL_BETA": "beta",
19+
"PY_RELEASE_LEVEL_GAMMA": "candidate",
20+
"PY_RELEASE_LEVEL_FINAL": "final",
21+
}
22+
23+
24+
class version_info(NamedTuple):
25+
major: int #: Major release number
26+
minor: int #: Minor release number
27+
micro: int #: Patch release number
28+
releaselevel: Literal["alpha", "beta", "candidate", "final"]
29+
serial: int #: Serial release number
30+
31+
32+
def get_header_version_info() -> version_info:
33+
# Capture PY_ prefixed #defines.
34+
pat = re.compile(r"\s*#define\s+(PY_\w*)\s+(\w+)", re.ASCII)
35+
36+
defines = {}
37+
patchlevel_h = PATCHLEVEL_H.read_text(encoding="utf-8")
38+
for line in patchlevel_h.splitlines():
39+
if (m := pat.match(line)) is not None:
40+
name, value = m.groups()
41+
defines[name] = value
42+
43+
return version_info(
44+
major=int(defines["PY_MAJOR_VERSION"]),
45+
minor=int(defines["PY_MINOR_VERSION"]),
46+
micro=int(defines["PY_MICRO_VERSION"]),
47+
releaselevel=RELEASE_LEVELS[defines["PY_RELEASE_LEVEL"]],
48+
serial=int(defines["PY_RELEASE_SERIAL"]),
49+
)
50+
51+
52+
def format_version_info(info: version_info) -> tuple[str, str]:
53+
version = f"{info.major}.{info.minor}"
54+
release = f"{info.major}.{info.minor}.{info.micro}"
55+
if info.releaselevel != "final":
56+
suffix = {"alpha": "a", "beta": "b", "candidate": "rc"}
57+
release += f"{suffix[info.releaselevel]}{info.serial}"
58+
return version, release
59+
60+
61+
def get_version_info():
62+
try:
63+
info = get_header_version_info()
64+
return format_version_info(info)
65+
except OSError:
66+
version, release = format_version_info(sys.version_info)
67+
print(
68+
f"Failed to get version info from Include/patchlevel.h, "
69+
f"using version of this interpreter ({release}).",
70+
file=sys.stderr,
71+
)
72+
return version, release
73+
74+
75+
def get_version_hex(info: version_info) -> int:
76+
"""Convert a version_info object to a hex version number."""
77+
levels = {"alpha": 0xA, "beta": 0xB, "candidate": 0xC, "final": 0xF}
78+
return (
79+
(info.major << 24)
80+
| (info.minor << 16)
81+
| (info.micro << 8)
82+
| (levels[info.releaselevel] << 4)
83+
| (info.serial << 0)
84+
)
85+
86+
87+
def parse_str_version(version: str) -> version_info:
88+
"""Convert a version string to a version_info object."""
89+
tag_cre = re.compile(r"(\d+)(?:\.(\d+)(?:\.(\d+))?)?(?:([ab]|rc)(\d+))?$")
90+
result = tag_cre.match(version)
91+
if not result:
92+
raise ValueError(f"Invalid version string: {version}")
93+
94+
parts = list(result.groups())
95+
levels = {"a": "alpha", "b": "beta", "rc": "candidate"}
96+
return version_info(
97+
major=int(parts[0]),
98+
minor=int(parts[1]),
99+
micro=int(parts[2] or 0),
100+
releaselevel=levels.get(parts[3], "final"),
101+
serial=int(parts[4]) if parts[4] else 0,
102+
)
103+
104+
105+
def parse_hex_version(version: int) -> version_info:
106+
"""Convert a hex version number to a version_info object."""
107+
if not isinstance(version, int):
108+
raise ValueError(f"Invalid hex version: {version}")
109+
110+
levels = {0xA: "alpha", 0xB: "beta", 0xC: "candidate", 0xF: "final"}
111+
return version_info(
112+
major=(version >> 24) & 0xFF,
113+
minor=(version >> 16) & 0xFF,
114+
micro=(version >> 8) & 0xFF,
115+
releaselevel=levels.get((version >> 4) & 0xF, "final"),
116+
serial=version & 0xF,
117+
)
118+
119+
120+
def main() -> None:
121+
import argparse
122+
123+
parser = argparse.ArgumentParser(color=True)
124+
parser.add_argument(
125+
"version",
126+
nargs="?",
127+
help="version to convert (default: repo version)",
128+
)
129+
group = parser.add_mutually_exclusive_group()
130+
group.add_argument(
131+
"--short",
132+
action="store_true",
133+
help="print version as x.y",
134+
)
135+
group.add_argument(
136+
"--hex",
137+
action="store_true",
138+
help="print version as a 4-byte hex number",
139+
)
140+
args = parser.parse_args()
141+
142+
if args.version:
143+
try:
144+
info = parse_str_version(args.version)
145+
except ValueError:
146+
info = parse_hex_version(int(args.version, 16))
147+
else:
148+
info = get_header_version_info()
149+
150+
short_ver, full_ver = format_version_info(info)
151+
if args.short:
152+
print(short_ver)
153+
elif args.hex:
154+
print(hex(get_version_hex(info)))
155+
else:
156+
print(full_ver)
157+
158+
159+
if __name__ == "__main__":
160+
main()

0 commit comments

Comments
 (0)