Last active
July 3, 2024 09:10
-
-
Save StSav012/5bf0a4e5f1bfc2d2df21ac2bc52247c9 to your computer and use it in GitHub Desktop.
Update Python from https://github.com/adang1345/PythonWin7
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding=utf-8 | |
from __future__ import annotations | |
import logging | |
import ssl | |
from http import HTTPStatus | |
from http.client import HTTPResponse | |
from json import loads | |
from pathlib import Path | |
from platform import uname, uname_result | |
from subprocess import run | |
from sys import exc_info, version_info | |
from tempfile import TemporaryDirectory | |
from traceback import format_exception | |
from typing import Iterable, Literal, cast, TYPE_CHECKING | |
from urllib import request | |
from packaging.version import Version | |
logger: logging.Logger = logging.getLogger(Path(__file__).name) | |
TIMEOUT: float = 10 | |
if TYPE_CHECKING: | |
CommitDataType = dict[ | |
str, | |
str | |
| dict[str, bool | int | str] | |
| dict[str, int | str | dict[str, bool | str] | dict[str, str]] | |
| dict[str, int] | |
| list[dict[str, int | str]] | |
| list[dict[str, str]], | |
] | |
def no_cert_context() -> ssl.SSLContext: | |
context: ssl.SSLContext = ssl.create_default_context() | |
context.check_hostname = False | |
context.verify_mode = ssl.CERT_NONE | |
return context | |
def get_github_commit_data( | |
user: str, | |
repo_name: str, | |
commit: str = "master", | |
) -> CommitDataType: | |
url: str = f"https://api.github.com/repos/{user}/{repo_name}/commits/{commit}" | |
logger.debug(f"Requesting {url}") | |
r: HTTPResponse | |
with request.urlopen(url, timeout=TIMEOUT) as r: | |
logger.debug(f"{url!r}: Response code: {r.getcode()}") | |
if r.getcode() != HTTPStatus.OK: | |
logger.warning(f"{url!r}: Response code is not OK: {r.getcode()}") | |
return {} | |
content: bytes = r.read() | |
if not content: | |
logger.warning(f"No data received from {url}") | |
return {} | |
d: CommitDataType = loads(content) | |
return d | |
def get_recent_version( | |
files_data: list[dict[str, int | str]], | |
is64bit: bool, | |
embed: bool = False, | |
extension: Literal[".exe", ".zip"] = ".exe", | |
restrict_to: Version | None = None, | |
) -> tuple[dict[str, int | str], Version] | tuple[None, None]: | |
newest_version: Version | None = None | |
newest_file: dict[str, int | str] | None = None | |
for file in files_data: | |
if not file["filename"].endswith(extension): | |
continue | |
if file["filename"].count("/") != 1: | |
logger.warning(f"Invalid filename: {file['filename']!r}") | |
continue | |
this_version_str: str | |
filename: str | |
this_version_str, filename = file["filename"].split("/", maxsplit=1) | |
if is64bit and "amd64" not in filename: | |
continue | |
if embed != ("embed" in filename): | |
continue | |
this_version: Version = Version(this_version_str) | |
if restrict_to is not None and not this_version.public.startswith( | |
restrict_to.public | |
): | |
continue | |
if newest_version is None or newest_version < this_version: | |
newest_version = this_version | |
newest_file = file | |
return newest_file, newest_version | |
def get_github_file(file_data: dict[str, int | str]) -> tuple[bytes, str]: | |
if ( | |
"contents_url" not in file_data | |
and "raw_url" not in file_data | |
and "blob_url" not in file_data | |
): | |
raise ValueError("No URL given") | |
size: int | None = None | |
url: str = "" | |
filename: str = "" | |
content: bytes | |
r: HTTPResponse | |
if "contents_url" in file_data: | |
url = file_data["contents_url"] | |
with request.urlopen(url, timeout=TIMEOUT, context=no_cert_context()) as r: | |
logger.debug(f"{url!r}: Response code: {r.getcode()}") | |
if r.getcode() != HTTPStatus.OK: | |
logger.warning(f"{url!r}: Response code is not OK: {r.getcode()}") | |
else: | |
content = r.read() | |
if content: | |
file_description: dict[str, str | dict[str, str]] = loads(content) | |
url = file_description.get("download_url", "") | |
if ( | |
"sha" in file_description | |
and file_description["sha"] != file_data["sha"] | |
): | |
logger.warning(f"{url!r}: SHA mismatch") | |
if "size" in file_description: | |
size = cast(int, file_description["size"]) | |
if "name" in file_description: | |
filename = file_description["name"] | |
if not url: | |
url = file_data["raw_url"] or file_data["blob_url"] | |
if not filename: | |
filename = url.split("/")[-1] | |
with request.urlopen(url, timeout=TIMEOUT) as r: | |
logger.debug(f"{url!r}: Response code: {r.getcode()}") | |
if r.getcode() != HTTPStatus.OK: | |
logger.warning(f"{url!r}: Response code is not OK: {r.getcode()}") | |
return b"", filename | |
content = r.read() | |
if size is not None and len(content) != size: | |
logger.warning(f"{url!r}: size mismatch: {size} != {len(content)}") | |
return content, filename | |
def install( | |
micro_only: bool = True, | |
*, | |
quiet: bool = True, | |
all_users: bool = False, | |
prepend_path: bool = True, | |
include_doc: bool = False, | |
include_test: bool = False, | |
other_args: Iterable[str] = (), | |
) -> int: | |
u: uname_result = uname() | |
if (u.system, u.release) != ("Windows", "7"): | |
raise NotImplementedError("Only Windows 7 is supported") | |
most_recent_file_data: dict[str, int | str] | None = None | |
most_recent_file_version: Version | None = None | |
commit: str = "master" | |
while most_recent_file_data is None or most_recent_file_version is None: | |
commit_data: CommitDataType = get_github_commit_data("adang1345", "PythonWin7", commit) | |
all_files: list[dict[str, int | str]] = commit_data.get("files", []) | |
most_recent_file_data, most_recent_file_version = get_recent_version( | |
all_files, | |
is64bit=(u.machine in ("AMD64", "x86_64")), | |
restrict_to=( | |
Version(".".join(map(str, [version_info.major, version_info.minor]))) | |
if micro_only | |
else None | |
), | |
) | |
parents: list[dict[str, str]] = commit_data.get("parents", []) | |
if parents: | |
commit = parents[0].get("sha", "") | |
if not commit: | |
logger.error("Malformed commit data occurred") | |
else: | |
logger.warning("Reached the oldest commit") | |
break | |
if most_recent_file_data is None: | |
logger.warning("No matching release found") | |
return 1 | |
logger.info(f"Found Python {most_recent_file_version} installer") | |
print(f"Found Python {most_recent_file_version} installer") | |
current_python_version: Version = Version( | |
".".join(map(str, filter(lambda v: v != "final", version_info))) | |
) | |
if current_python_version >= most_recent_file_version: | |
print( | |
f"Current Python version {current_python_version} is new enough. Exiting…" | |
) | |
return 0 | |
logger.info(f"Downloading Python {most_recent_file_version} installer…") | |
print(f"Downloading Python {most_recent_file_version} installer…") | |
data, name = get_github_file(most_recent_file_data) | |
logger.info(f"Done. Installing Python {most_recent_file_version}…") | |
print(f"Done. Installing Python {most_recent_file_version}…") | |
with TemporaryDirectory() as tmpdir: | |
pe_file: Path = Path(tmpdir) / name | |
pe_file.write_bytes(data) | |
try: | |
ret: int = run( | |
[pe_file] | |
+ (["/quiet"] if quiet else []) | |
+ [ | |
f"InstallAllUsers={int(all_users)}", | |
f"PrependPath={int(prepend_path)}", | |
f"Include_doc={int(include_doc)}", | |
f"Include_test={int(include_test)}", | |
] | |
+ list(other_args), | |
cwd=tmpdir, | |
check=True, | |
).returncode | |
except Exception as ex: | |
logger.error(ex) | |
if isinstance(ex, FileNotFoundError): | |
logger.error(ex.filename or pe_file) | |
logger.error("".join(format_exception(*exc_info()))) | |
input("Press Enter to continue") | |
else: | |
logger.info("Done") | |
print("Done") | |
return ret | |
if __name__ == "__main__": | |
def main() -> int: | |
from argparse import ArgumentParser, Namespace | |
parser: ArgumentParser = ArgumentParser( | |
description="Windows 7 only! Update Python from https://github.com/adang1345/PythonWin7.", | |
epilog=" ".join( | |
( | |
"Other arguments are passed directly to the Python installer", | |
"and may supersede the parameters above.", | |
"See https://docs.python.org/3/using/windows.html#installing-without-ui.", | |
) | |
), | |
prefix_chars="-/", | |
) | |
parser.add_argument( | |
*("-F", "/F"), | |
action="store_true", | |
help="Install the newest available version, regardless of the currently installed one", | |
) | |
parser.add_argument( | |
*("-q", "--quiet", "/quiet"), | |
action="store_true", | |
help="Install without displaying any UI", | |
) | |
parser.add_argument( | |
"--all-users", | |
action="store_true", | |
help="Perform a system-wide installation (InstallAllUsers=1)", | |
) | |
parser.add_argument( | |
"--prepend-path", | |
action="store_true", | |
help="Prepend install and Scripts directories to PATH and add .PY to PATHEXT (PrependPath=1)", | |
) | |
parser.add_argument( | |
"--include-doc", | |
action="store_true", | |
help="Install Python manual (Include_doc=1)", | |
) | |
parser.add_argument( | |
"--include-test", | |
action="store_true", | |
help="Install standard library test suite (Include_test=1)", | |
) | |
args: Namespace | |
leftovers: list[str] | |
args, leftovers = parser.parse_known_args() | |
for a in leftovers.copy(): | |
if "=" not in a: | |
continue | |
k, v = a.split("=", maxsplit=1) | |
if k == "InstallAllUsers": | |
args.all_users = bool(int(v)) | |
leftovers.remove(a) | |
elif k == "PrependPath": | |
args.prepend_path = bool(int(v)) | |
leftovers.remove(a) | |
elif k == "Include_doc": | |
args.include_doc = bool(int(v)) | |
leftovers.remove(a) | |
elif k == "Include_test": | |
args.include_test = bool(int(v)) | |
leftovers.remove(a) | |
return install( | |
micro_only=not args.F, | |
quiet=args.quiet, | |
all_users=args.all_users, | |
prepend_path=args.prepend_path, | |
include_doc=args.include_doc, | |
include_test=args.include_test, | |
other_args=leftovers, | |
) | |
exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment