]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
GH-121970: Modernise the patchlevel extension (#121995)
authorAdam Turner <9087854+AA-Turner@users.noreply.github.com>
Sat, 20 Jul 2024 13:44:43 +0000 (14:44 +0100)
committerGitHub <noreply@github.com>
Sat, 20 Jul 2024 13:44:43 +0000 (13:44 +0000)
Doc/.ruff.toml
Doc/conf.py
Doc/tools/extensions/patchlevel.py

index 5c40d90b8f000b7ea2caaaa405a9c6ddfd2905ba..24f1c4f2ff6801ff9bf09e5f1244806ae0892615 100644 (file)
@@ -6,7 +6,6 @@ extend-exclude = [
     "includes/*",
     # Temporary exclusions:
     "tools/extensions/escape4chm.py",
-    "tools/extensions/patchlevel.py",
     "tools/extensions/pyspecific.py",
 ]
 
index dfa873274b69bf31a8fdf76dc4ac374cf2c294b9..17e98e1a01ed21acd53b372091919379848cbfeb 100644 (file)
@@ -6,6 +6,7 @@
 # The contents of this file are pickled, so don't put values in the namespace
 # that aren't pickleable (module imports are okay, they're removed automatically).
 
+import importlib
 import os
 import sys
 import time
@@ -64,9 +65,8 @@ copyright = f"2001-{time.strftime('%Y')}, Python Software Foundation"
 
 # We look for the Include/patchlevel.h file in the current Python source tree
 # and replace the values accordingly.
-import patchlevel  # noqa: E402
-
-version, release = patchlevel.get_version_info()
+# See Doc/tools/extensions/patchlevel.py
+version, release = importlib.import_module('patchlevel').get_version_info()
 
 rst_epilog = f"""
 .. |python_version_literal| replace:: ``Python {version}``
index 617f28c2527ddff98eb308addf2addf17779be23..f2df6db47a222712bc09258b79010452216438bc 100644 (file)
@@ -1,68 +1,77 @@
-# -*- coding: utf-8 -*-
-"""
-    patchlevel.py
-    ~~~~~~~~~~~~~
+"""Extract version information from Include/patchlevel.h."""
 
-    Extract version info from Include/patchlevel.h.
-    Adapted from Doc/tools/getversioninfo.
+import re
+import sys
+from pathlib import Path
+from typing import Literal, NamedTuple
 
-    :copyright: 2007-2008 by Georg Brandl.
-    :license: Python license.
-"""
+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"
 
-from __future__ import print_function
+RELEASE_LEVELS = {
+    "PY_RELEASE_LEVEL_ALPHA": "alpha",
+    "PY_RELEASE_LEVEL_BETA": "beta",
+    "PY_RELEASE_LEVEL_GAMMA": "candidate",
+    "PY_RELEASE_LEVEL_FINAL": "final",
+}
 
-import os
-import re
-import sys
 
-def get_header_version_info(srcdir):
-    patchlevel_h = os.path.join(srcdir, '..', 'Include', 'patchlevel.h')
+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
 
-    # This won't pick out all #defines, but it will pick up the ones we
-    # care about.
-    rx = re.compile(r'\s*#define\s+([a-zA-Z][a-zA-Z_0-9]*)\s+([a-zA-Z_0-9]+)')
 
-    d = {}
-    with open(patchlevel_h) as f:
-        for line in f:
-            m = rx.match(line)
-            if m is not None:
-                name, value = m.group(1, 2)
-                d[name] = value
+def get_header_version_info() -> version_info:
+    # Capture PY_ prefixed #defines.
+    pat = re.compile(r"\s*#define\s+(PY_\w*)\s+(\w+)", re.ASCII)
 
-    release = version = '%s.%s' % (d['PY_MAJOR_VERSION'], d['PY_MINOR_VERSION'])
-    micro = int(d['PY_MICRO_VERSION'])
-    release += '.' + str(micro)
+    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
 
-    level = d['PY_RELEASE_LEVEL']
-    suffixes = {
-        'PY_RELEASE_LEVEL_ALPHA': 'a',
-        'PY_RELEASE_LEVEL_BETA':  'b',
-        'PY_RELEASE_LEVEL_GAMMA': 'rc',
-        }
-    if level != 'PY_RELEASE_LEVEL_FINAL':
-        release += suffixes[level] + str(int(d['PY_RELEASE_SERIAL']))
-    return version, release
+    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 get_sys_version_info():
-    major, minor, micro, level, serial = sys.version_info
-    release = version = '%s.%s' % (major, minor)
-    release += '.%s' % micro
-    if level != 'final':
-        release += '%s%s' % (level[0], 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:
-        return get_header_version_info('.')
-    except (IOError, OSError):
-        version, release = get_sys_version_info()
-        print('Can\'t get version info from Include/patchlevel.h, ' \
-              'using version of this interpreter (%s).' % release, file=sys.stderr)
+        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(get_header_version_info('.')[1])
+
+if __name__ == "__main__":
+    print(format_version_info(get_header_version_info())[1])