| 1 | |
| 2 | |
| 3 | |
| 4 |
|
| 5 | |
| 6 | Handle Python version check logic.
|
| 7 |
|
| 8 | Not all Python versions are supported by scripts. Yet, on some cases,
|
| 9 | like during documentation build, a newer version of python could be
|
| 10 | available.
|
| 11 |
|
| 12 | This class allows checking if the minimal requirements are followed.
|
| 13 |
|
| 14 | Better than that, PythonVersion.check_python() not only checks the minimal
|
| 15 | requirements, but it automatically switches to a the newest available
|
| 16 | Python version if present.
|
| 17 |
|
| 18 |
|
| 19 |
|
| 20 | import os
|
| 21 | import re
|
| 22 | import subprocess
|
| 23 | import shlex
|
| 24 | import sys
|
| 25 |
|
| 26 | from glob import glob
|
| 27 | from textwrap import indent
|
| 28 |
|
| 29 | class PythonVersion:
|
| 30 | |
| 31 | Ancillary methods that checks for missing dependencies for different
|
| 32 | types of types, like binaries, python modules, rpm deps, etc.
|
| 33 |
|
| 34 |
|
| 35 | def __init__(self, version):
|
| 36 | |
| 37 | Ïnitialize self.version tuple from a version string.
|
| 38 |
|
| 39 | self.version = self.parse_version(version)
|
| 40 |
|
| 41 | @staticmethod
|
| 42 | def parse_version(version):
|
| 43 | |
| 44 | Convert a major.minor.patch version into a tuple.
|
| 45 |
|
| 46 | return tuple(int(x) for x in version.split("."))
|
| 47 |
|
| 48 | @staticmethod
|
| 49 | def ver_str(version):
|
| 50 | |
| 51 | Returns a version tuple as major.minor.patch.
|
| 52 |
|
| 53 | return ".".join([str(x) for x in version])
|
| 54 |
|
| 55 | @staticmethod
|
| 56 | def cmd_print(cmd, max_len=80):
|
| 57 | |
| 58 | Outputs a command line, repecting maximum width.
|
| 59 |
|
| 60 |
|
| 61 | cmd_line = []
|
| 62 |
|
| 63 | for w in cmd:
|
| 64 | w = shlex.quote(w)
|
| 65 |
|
| 66 | if cmd_line:
|
| 67 | if not max_len or len(cmd_line[-1]) + len(w) < max_len:
|
| 68 | cmd_line[-1] += " " + w
|
| 69 | continue
|
| 70 | else:
|
| 71 | cmd_line[-1] += " \\"
|
| 72 | cmd_line.append(w)
|
| 73 | else:
|
| 74 | cmd_line.append(w)
|
| 75 |
|
| 76 | return "\n ".join(cmd_line)
|
| 77 |
|
| 78 | def __str__(self):
|
| 79 | |
| 80 | Return a version tuple as major.minor.patch from self.version.
|
| 81 |
|
| 82 | return self.ver_str(self.version)
|
| 83 |
|
| 84 | @staticmethod
|
| 85 | def get_python_version(cmd):
|
| 86 | |
| 87 | Get python version from a Python binary. As we need to detect if
|
| 88 | are out there newer python binaries, we can't rely on sys.release here.
|
| 89 |
|
| 90 |
|
| 91 | kwargs = {}
|
| 92 | if sys.version_info < (3, 7):
|
| 93 | kwargs['universal_newlines'] = True
|
| 94 | else:
|
| 95 | kwargs['text'] = True
|
| 96 |
|
| 97 | result = subprocess.run([cmd, "--version"],
|
| 98 | stdout = subprocess.PIPE,
|
| 99 | stderr = subprocess.PIPE,
|
| 100 | **kwargs, check=False)
|
| 101 |
|
| 102 | version = result.stdout.strip()
|
| 103 |
|
| 104 | match = re.search(r"(\d+\.\d+\.\d+)", version)
|
| 105 | if match:
|
| 106 | return PythonVersion.parse_version(match.group(1))
|
| 107 |
|
| 108 | print(f"Can't parse version {version}")
|
| 109 | return (0, 0, 0)
|
| 110 |
|
| 111 | @staticmethod
|
| 112 | def find_python(min_version):
|
| 113 | |
| 114 | Detect if are out there any python 3.xy version newer than the
|
| 115 | current one.
|
| 116 |
|
| 117 | Note: this routine is limited to up to 2 digits for python3. We
|
| 118 | may need to update it one day, hopefully on a distant future.
|
| 119 |
|
| 120 | patterns = [
|
| 121 | "python3.[0-9][0-9]",
|
| 122 | "python3.[0-9]",
|
| 123 | ]
|
| 124 |
|
| 125 | python_cmd = []
|
| 126 |
|
| 127 | |
| 128 | for path in os.getenv("PATH", "").split(":"):
|
| 129 | for pattern in patterns:
|
| 130 | for cmd in glob(os.path.join(path, pattern)):
|
| 131 | if os.path.isfile(cmd) and os.access(cmd, os.X_OK):
|
| 132 | version = PythonVersion.get_python_version(cmd)
|
| 133 | if version >= min_version:
|
| 134 | python_cmd.append((version, cmd))
|
| 135 |
|
| 136 | return sorted(python_cmd, reverse=True)
|
| 137 |
|
| 138 | @staticmethod
|
| 139 | def check_python(min_version, show_alternatives=False, bail_out=False,
|
| 140 | success_on_error=False):
|
| 141 | |
| 142 | Check if the current python binary satisfies our minimal requirement
|
| 143 | for Sphinx build. If not, re-run with a newer version if found.
|
| 144 |
|
| 145 | cur_ver = sys.version_info[:3]
|
| 146 | if cur_ver >= min_version:
|
| 147 | ver = PythonVersion.ver_str(cur_ver)
|
| 148 | return
|
| 149 |
|
| 150 | python_ver = PythonVersion.ver_str(cur_ver)
|
| 151 |
|
| 152 | available_versions = PythonVersion.find_python(min_version)
|
| 153 | if not available_versions:
|
| 154 | print(f"ERROR: Python version {python_ver} is not supported anymore\n")
|
| 155 | print(" Can't find a new version. This script may fail")
|
| 156 | return
|
| 157 |
|
| 158 | script_path = os.path.abspath(sys.argv[0])
|
| 159 |
|
| 160 | |
| 161 | if available_versions:
|
| 162 | new_python_cmd = available_versions[0][1]
|
| 163 | else:
|
| 164 | new_python_cmd = None
|
| 165 |
|
| 166 | if show_alternatives and available_versions:
|
| 167 | print("You could run, instead:")
|
| 168 | for _, cmd in available_versions:
|
| 169 | args = [cmd, script_path] + sys.argv[1:]
|
| 170 |
|
| 171 | cmd_str = indent(PythonVersion.cmd_print(args), " ")
|
| 172 | print(f"{cmd_str}\n")
|
| 173 |
|
| 174 | if bail_out:
|
| 175 | msg = f"Python {python_ver} not supported. Bailing out"
|
| 176 | if success_on_error:
|
| 177 | print(msg, file=sys.stderr)
|
| 178 | sys.exit(0)
|
| 179 | else:
|
| 180 | sys.exit(msg)
|
| 181 |
|
| 182 | print(f"Python {python_ver} not supported. Changing to {new_python_cmd}")
|
| 183 |
|
| 184 | |
| 185 | args = [new_python_cmd, script_path] + sys.argv[1:]
|
| 186 |
|
| 187 | try:
|
| 188 | os.execv(new_python_cmd, args)
|
| 189 | except OSError as e:
|
| 190 | sys.exit(f"Failed to restart with {new_python_cmd}: {e}")
|