This commit is contained in:
lz_db
2026-10-16 22:48:30 +08:00
parent 190de72571
commit 0b36fd3383
3161 changed files with 12787 additions and 77274 deletions
@@ -4,35 +4,22 @@ Miscellaneous utility functions -- anything that doesn't fit into
one of the other *util.py modules.
"""
from __future__ import annotations
import functools
import importlib.util
import os
import pathlib
import re
import string
import subprocess
import sys
import sysconfig
import tempfile
from collections.abc import Callable, Iterable, Mapping
from typing import TYPE_CHECKING, AnyStr
import functools
from jaraco.functools import pass_none
from ._log import log
from ._modified import newer
from .errors import DistutilsByteCompileError, DistutilsPlatformError
from .spawn import spawn
if TYPE_CHECKING:
from typing_extensions import TypeVarTuple, Unpack
_Ts = TypeVarTuple("_Ts")
from distutils.errors import DistutilsPlatformError, DistutilsByteCompileError
from distutils.dep_util import newer
from distutils.spawn import spawn
from distutils import log
def get_host_platform() -> str:
def get_host_platform():
"""
Return a string that identifies the current platform. Use this
function to distinguish platform-specific build directories and
@@ -41,12 +28,27 @@ def get_host_platform() -> str:
# This function initially exposed platforms as defined in Python 3.9
# even with older Python versions when distutils was split out.
# Now it delegates to stdlib sysconfig.
# Now it delegates to stdlib sysconfig, but maintains compatibility.
if sys.version_info < (3, 8):
if os.name == 'nt':
if '(arm)' in sys.version.lower():
return 'win-arm32'
if '(arm64)' in sys.version.lower():
return 'win-arm64'
if sys.version_info < (3, 9):
if os.name == "posix" and hasattr(os, 'uname'):
osname, host, release, version, machine = os.uname()
if osname[:3] == "aix":
from .py38compat import aix_platform
return aix_platform(osname, version, release)
return sysconfig.get_platform()
def get_platform() -> str:
def get_platform():
if os.name == 'nt':
TARGET_TO_PLAT = {
'x86': 'win32',
@@ -107,41 +109,49 @@ def get_macosx_target_ver():
):
my_msg = (
'$' + MACOSX_VERSION_VAR + ' mismatch: '
f'now "{env_ver}" but "{syscfg_ver}" during configure; '
'must use 10.3 or later'
'now "%s" but "%s" during configure; '
'must use 10.3 or later' % (env_ver, syscfg_ver)
)
raise DistutilsPlatformError(my_msg)
return env_ver
return syscfg_ver
def split_version(s: str) -> list[int]:
def split_version(s):
"""Convert a dot-separated string into a list of numbers for comparisons"""
return [int(n) for n in s.split('.')]
@pass_none
def convert_path(pathname: str | os.PathLike[str]) -> str:
r"""
Allow for pathlib.Path inputs, coax to a native path string.
If None is passed, will just pass it through as
Setuptools relies on this behavior.
>>> convert_path(None) is None
True
Removes empty paths.
>>> convert_path('foo/./bar').replace('\\', '/')
'foo/bar'
def convert_path(pathname):
"""Return 'pathname' as a name that will work on the native filesystem,
i.e. split it on '/' and put it back together again using the current
directory separator. Needed because filenames in the setup script are
always supplied in Unix style, and have to be converted to the local
convention before we can actually use them in the filesystem. Raises
ValueError on non-Unix-ish systems if 'pathname' either starts or
ends with a slash.
"""
return os.fspath(pathlib.PurePath(pathname))
if os.sep == '/':
return pathname
if not pathname:
return pathname
if pathname[0] == '/':
raise ValueError("path '%s' cannot be absolute" % pathname)
if pathname[-1] == '/':
raise ValueError("path '%s' cannot end with '/'" % pathname)
paths = pathname.split('/')
while '.' in paths:
paths.remove('.')
if not paths:
return os.curdir
return os.path.join(*paths)
def change_root(
new_root: AnyStr | os.PathLike[AnyStr], pathname: AnyStr | os.PathLike[AnyStr]
) -> AnyStr:
# convert_path ()
def change_root(new_root, pathname):
"""Return 'pathname' with 'new_root' prepended. If 'pathname' is
relative, this is equivalent to "os.path.join(new_root,pathname)".
Otherwise, it requires making 'pathname' relative and then joining the
@@ -155,15 +165,15 @@ def change_root(
elif os.name == 'nt':
(drive, path) = os.path.splitdrive(pathname)
if path[0] == os.sep:
if path[0] == '\\':
path = path[1:]
return os.path.join(new_root, path)
raise DistutilsPlatformError(f"nothing known about platform '{os.name}'")
@functools.lru_cache
def check_environ() -> None:
@functools.lru_cache()
def check_environ():
"""Ensure that 'os.environ' has all the environment variables we
guarantee that users can use in config files, command-line options,
etc. Currently this includes:
@@ -185,7 +195,7 @@ def check_environ() -> None:
os.environ['PLAT'] = get_platform()
def subst_vars(s, local_vars: Mapping[str, object]) -> str:
def subst_vars(s, local_vars):
"""
Perform variable substitution on 'string'.
Variables are indicated by format-style braces ("{var}").
@@ -218,13 +228,13 @@ def _subst_compat(s):
import warnings
warnings.warn(
"shell/Perl-style substitutions are deprecated",
"shell/Perl-style substitions are deprecated",
DeprecationWarning,
)
return repl
def grok_environment_error(exc: object, prefix: str = "error: ") -> str:
def grok_environment_error(exc, prefix="error: "):
# Function kept for backward compatibility.
# Used to try clever things with EnvironmentErrors,
# but nowadays str(exception) produces good messages.
@@ -237,12 +247,12 @@ _wordchars_re = _squote_re = _dquote_re = None
def _init_regex():
global _wordchars_re, _squote_re, _dquote_re
_wordchars_re = re.compile(rf'[^\\\'\"{string.whitespace} ]*')
_wordchars_re = re.compile(r'[^\\\'\"%s ]*' % string.whitespace)
_squote_re = re.compile(r"'(?:[^'\\]|\\.)*'")
_dquote_re = re.compile(r'"(?:[^"\\]|\\.)*"')
def split_quoted(s: str) -> list[str]:
def split_quoted(s):
"""Split a string up according to Unix shell-like rules for quotes and
backslashes. In short: words are delimited by spaces, as long as those
spaces are not escaped by a backslash, or inside a quoted string.
@@ -289,10 +299,10 @@ def split_quoted(s: str) -> list[str]:
elif s[end] == '"': # slurp doubly-quoted string
m = _dquote_re.match(s, end)
else:
raise RuntimeError(f"this can't happen (bad char '{s[end]}')")
raise RuntimeError("this can't happen (bad char '%c')" % s[end])
if m is None:
raise ValueError(f"bad string (mismatched {s[end]} quotes?)")
raise ValueError("bad string (mismatched %s quotes?)" % s[end])
(beg, end) = m.span()
s = s[:beg] + s[beg + 1 : end - 1] + s[end:]
@@ -308,24 +318,17 @@ def split_quoted(s: str) -> list[str]:
# split_quoted ()
def execute(
func: Callable[[Unpack[_Ts]], object],
args: tuple[Unpack[_Ts]],
msg: object = None,
verbose: bool = False,
dry_run: bool = False,
) -> None:
"""
Perform some action that affects the outside world (e.g. by
writing to the filesystem). Such actions are special because they
are disabled by the 'dry_run' flag. This method handles that
complication; simply supply the
def execute(func, args, msg=None, verbose=0, dry_run=0):
"""Perform some action that affects the outside world (eg. by
writing to the filesystem). Such actions are special because they
are disabled by the 'dry_run' flag. This method takes care of all
that bureaucracy for you; all you have to do is supply the
function to call and an argument tuple for it (to embody the
"external action" being performed) and an optional message to
emit.
"external action" being performed), and an optional message to
print.
"""
if msg is None:
msg = f"{func.__name__}{args!r}"
msg = "{}{!r}".format(func.__name__, args)
if msg[-2:] == ',)': # correct for singleton tuple
msg = msg[0:-2] + ')'
@@ -334,7 +337,7 @@ def execute(
func(*args)
def strtobool(val: str) -> bool:
def strtobool(val):
"""Convert a string representation of truth to true (1) or false (0).
True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
@@ -343,23 +346,23 @@ def strtobool(val: str) -> bool:
"""
val = val.lower()
if val in ('y', 'yes', 't', 'true', 'on', '1'):
return True
return 1
elif val in ('n', 'no', 'f', 'false', 'off', '0'):
return False
return 0
else:
raise ValueError(f"invalid truth value {val!r}")
raise ValueError("invalid truth value {!r}".format(val))
def byte_compile( # noqa: C901
py_files: Iterable[str],
optimize: int = 0,
force: bool = False,
prefix: str | None = None,
base_dir: str | None = None,
verbose: bool = True,
dry_run: bool = False,
direct: bool | None = None,
) -> None:
py_files,
optimize=0,
force=0,
prefix=None,
base_dir=None,
verbose=1,
dry_run=0,
direct=None,
):
"""Byte-compile a collection of Python source files to .pyc
files in a __pycache__ subdirectory. 'py_files' is a list
of files to compile; any files that don't end in ".py" are silently
@@ -409,10 +412,20 @@ def byte_compile( # noqa: C901
# "Indirect" byte-compilation: write a temporary script and then
# run it with the appropriate flags.
if not direct:
(script_fd, script_name) = tempfile.mkstemp(".py")
try:
from tempfile import mkstemp
(script_fd, script_name) = mkstemp(".py")
except ImportError:
from tempfile import mktemp
(script_fd, script_name) = None, mktemp(".py")
log.info("writing byte-compilation script '%s'", script_name)
if not dry_run:
script = os.fdopen(script_fd, "w", encoding='utf-8')
if script_fd is not None:
script = os.fdopen(script_fd, "w")
else:
script = open(script_name, "w")
with script:
script.write(
@@ -434,19 +447,20 @@ files = [
script.write(",\n".join(map(repr, py_files)) + "]\n")
script.write(
f"""
byte_compile(files, optimize={optimize!r}, force={force!r},
prefix={prefix!r}, base_dir={base_dir!r},
verbose={verbose!r}, dry_run=False,
direct=True)
"""
byte_compile(files, optimize=%r, force=%r,
prefix=%r, base_dir=%r,
verbose=%r, dry_run=0,
direct=1)
"""
% (optimize, force, prefix, base_dir, verbose)
)
cmd = [sys.executable]
cmd.extend(subprocess._optim_args_from_interpreter_flags())
cmd.append(script_name)
spawn(cmd, dry_run=dry_run)
execute(os.remove, (script_name,), f"removing {script_name}", dry_run=dry_run)
execute(os.remove, (script_name,), "removing %s" % script_name, dry_run=dry_run)
# "Direct" byte-compilation: use the py_compile module to compile
# right here, right now. Note that the script generated in indirect
@@ -473,7 +487,8 @@ byte_compile(files, optimize={optimize!r}, force={force!r},
if prefix:
if file[: len(prefix)] != prefix:
raise ValueError(
f"invalid prefix: filename {file!r} doesn't start with {prefix!r}"
"invalid prefix: filename %r doesn't start with %r"
% (file, prefix)
)
dfile = dfile[len(prefix) :]
if base_dir:
@@ -489,30 +504,10 @@ byte_compile(files, optimize={optimize!r}, force={force!r},
log.debug("skipping byte-compilation of %s to %s", file, cfile_base)
def rfc822_escape(header: str) -> str:
def rfc822_escape(header):
"""Return a version of the string escaped for inclusion in an
RFC-822 header, by ensuring there are 8 spaces space after each newline.
"""
indent = 8 * " "
lines = header.splitlines(keepends=True)
# Emulate the behaviour of `str.split`
# (the terminal line break in `splitlines` does not result in an extra line):
ends_in_newline = lines and lines[-1].splitlines()[0] != lines[-1]
suffix = indent if ends_in_newline else ""
return indent.join(lines) + suffix
def is_mingw() -> bool:
"""Returns True if the current platform is mingw.
Python compiled with Mingw-w64 has sys.platform == 'win32' and
get_platform() starts with 'mingw'.
"""
return sys.platform == 'win32' and get_platform().startswith('mingw')
def is_freethreaded():
"""Return True if the Python interpreter is built with free threading support."""
return bool(sysconfig.get_config_var('Py_GIL_DISABLED'))
lines = header.split('\n')
sep = '\n' + 8 * ' '
return sep.join(lines)