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,31 +4,12 @@ Provides the Command class, the base class for the command classes
in the distutils.command package.
"""
from __future__ import annotations
import logging
import sys
import os
import re
import sys
from abc import abstractmethod
from collections.abc import Callable, MutableSequence
from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, overload
from . import _modified, archive_util, dir_util, file_util, util
from ._log import log
from .errors import DistutilsOptionError
if TYPE_CHECKING:
# type-only import because of mutual dependence between these classes
from distutils.dist import Distribution
from typing_extensions import TypeVarTuple, Unpack
_Ts = TypeVarTuple("_Ts")
_StrPathT = TypeVar("_StrPathT", bound="str | os.PathLike[str]")
_BytesPathT = TypeVar("_BytesPathT", bound="bytes | os.PathLike[bytes]")
_CommandT = TypeVar("_CommandT", bound="Command")
from distutils.errors import DistutilsOptionError
from distutils import util, dir_util, file_util, archive_util, dep_util
from distutils import log
class Command:
@@ -61,18 +42,11 @@ class Command:
# 'sub_commands' is usually defined at the *end* of a class, because
# predicates can be unbound methods, so they must already have been
# defined. The canonical example is the "install" command.
sub_commands: ClassVar[ # Any to work around variance issues
list[tuple[str, Callable[[Any], bool] | None]]
] = []
user_options: ClassVar[
# Specifying both because list is invariant. Avoids mypy override assignment issues
list[tuple[str, str, str]] | list[tuple[str, str | None, str]]
] = []
sub_commands = []
# -- Creation/initialization methods -------------------------------
def __init__(self, dist: Distribution) -> None:
def __init__(self, dist):
"""Create and initialize a new Command object. Most importantly,
invokes the 'initialize_options()' method, which is the real
initializer and depends on the actual command being
@@ -111,13 +85,13 @@ class Command:
# The 'help' flag is just used for command-line parsing, so
# none of that complicated bureaucracy is needed.
self.help = False
self.help = 0
# 'finalized' records whether or not 'finalize_options()' has been
# called. 'finalize_options()' itself should not pay attention to
# this flag: it is the business of 'ensure_finalized()', which
# always calls 'finalize_options()', to respect/update it.
self.finalized = False
self.finalized = 0
# XXX A more explicit way to customize dry_run would be better.
def __getattr__(self, attr):
@@ -130,10 +104,10 @@ class Command:
else:
raise AttributeError(attr)
def ensure_finalized(self) -> None:
def ensure_finalized(self):
if not self.finalized:
self.finalize_options()
self.finalized = True
self.finalized = 1
# Subclasses must define:
# initialize_options()
@@ -148,8 +122,7 @@ class Command:
# run the command: do whatever it is we're here to do,
# controlled by the command's various option values
@abstractmethod
def initialize_options(self) -> None:
def initialize_options(self):
"""Set default values for all the options that this command
supports. Note that these defaults may be overridden by other
commands, by the setup script, by config files, or by the
@@ -160,11 +133,10 @@ class Command:
This method must be implemented by all command classes.
"""
raise RuntimeError(
f"abstract method -- subclass {self.__class__} must override"
"abstract method -- subclass %s must override" % self.__class__
)
@abstractmethod
def finalize_options(self) -> None:
def finalize_options(self):
"""Set final values for all the options that this command supports.
This is always called as late as possible, ie. after any option
assignments from the command-line or from other commands have been
@@ -176,25 +148,24 @@ class Command:
This method must be implemented by all command classes.
"""
raise RuntimeError(
f"abstract method -- subclass {self.__class__} must override"
"abstract method -- subclass %s must override" % self.__class__
)
def dump_options(self, header=None, indent=""):
from distutils.fancy_getopt import longopt_xlate
if header is None:
header = f"command options for '{self.get_command_name()}':"
self.announce(indent + header, level=logging.INFO)
header = "command options for '%s':" % self.get_command_name()
self.announce(indent + header, level=log.INFO)
indent = indent + " "
for option, _, _ in self.user_options:
for (option, _, _) in self.user_options:
option = option.translate(longopt_xlate)
if option[-1] == "=":
option = option[:-1]
value = getattr(self, option)
self.announce(indent + f"{option} = {value}", level=logging.INFO)
self.announce(indent + "{} = {}".format(option, value), level=log.INFO)
@abstractmethod
def run(self) -> None:
def run(self):
"""A command's raison d'etre: carry out the action it exists to
perform, controlled by the options initialized in
'initialize_options()', customized by other commands, the setup
@@ -205,13 +176,16 @@ class Command:
This method must be implemented by all command classes.
"""
raise RuntimeError(
f"abstract method -- subclass {self.__class__} must override"
"abstract method -- subclass %s must override" % self.__class__
)
def announce(self, msg: object, level: int = logging.DEBUG) -> None:
def announce(self, msg, level=1):
"""If the current verbosity level is of greater than or equal to
'level' print 'msg' to stdout.
"""
log.log(level, msg)
def debug_print(self, msg: object) -> None:
def debug_print(self, msg):
"""Print 'msg' to stdout if the global DEBUG (taken from the
DISTUTILS_DEBUG environment variable) flag is true.
"""
@@ -240,16 +214,18 @@ class Command:
setattr(self, option, default)
return default
elif not isinstance(val, str):
raise DistutilsOptionError(f"'{option}' must be a {what} (got `{val}`)")
raise DistutilsOptionError(
"'{}' must be a {} (got `{}`)".format(option, what, val)
)
return val
def ensure_string(self, option: str, default: str | None = None) -> None:
def ensure_string(self, option, default=None):
"""Ensure that 'option' is a string; if not defined, set it to
'default'.
"""
self._ensure_stringlike(option, "string", default)
def ensure_string_list(self, option: str) -> None:
def ensure_string_list(self, option):
r"""Ensure that 'option' is a list of strings. If 'option' is
currently a string, we split it either on /,\s*/ or /\s+/, so
"foo bar baz", "foo,bar,baz", and "foo, bar baz" all become
@@ -267,7 +243,7 @@ class Command:
ok = False
if not ok:
raise DistutilsOptionError(
f"'{option}' must be a list of strings (got {val!r})"
"'{}' must be a list of strings (got {!r})".format(option, val)
)
def _ensure_tested_string(self, option, tester, what, error_fmt, default=None):
@@ -277,13 +253,13 @@ class Command:
("error in '%s' option: " + error_fmt) % (option, val)
)
def ensure_filename(self, option: str) -> None:
def ensure_filename(self, option):
"""Ensure that 'option' is the name of an existing file."""
self._ensure_tested_string(
option, os.path.isfile, "filename", "'%s' does not exist or is not a file"
)
def ensure_dirname(self, option: str) -> None:
def ensure_dirname(self, option):
self._ensure_tested_string(
option,
os.path.isdir,
@@ -293,15 +269,13 @@ class Command:
# -- Convenience methods for commands ------------------------------
def get_command_name(self) -> str:
def get_command_name(self):
if hasattr(self, 'command_name'):
return self.command_name
else:
return self.__class__.__name__
def set_undefined_options(
self, src_cmd: str, *option_pairs: tuple[str, str]
) -> None:
def set_undefined_options(self, src_cmd, *option_pairs):
"""Set the values of any "undefined" options from corresponding
option values in some other command object. "Undefined" here means
"is None", which is the convention used to indicate that an option
@@ -318,13 +292,11 @@ class Command:
# Option_pairs: list of (src_option, dst_option) tuples
src_cmd_obj = self.distribution.get_command_obj(src_cmd)
src_cmd_obj.ensure_finalized()
for src_option, dst_option in option_pairs:
for (src_option, dst_option) in option_pairs:
if getattr(self, dst_option) is None:
setattr(self, dst_option, getattr(src_cmd_obj, src_option))
# NOTE: Because distutils is private to Setuptools and not all commands are exposed here,
# not every possible command is enumerated in the signature.
def get_finalized_command(self, command: str, create: bool = True) -> Command:
def get_finalized_command(self, command, create=1):
"""Wrapper around Distribution's 'get_command_obj()' method: find
(create if necessary and 'create' is true) the command object for
'command', call its 'ensure_finalized()' method, and return the
@@ -336,27 +308,17 @@ class Command:
# XXX rename to 'get_reinitialized_command()'? (should do the
# same in dist.py, if so)
@overload
def reinitialize_command(
self, command: str, reinit_subcommands: bool = False
) -> Command: ...
@overload
def reinitialize_command(
self, command: _CommandT, reinit_subcommands: bool = False
) -> _CommandT: ...
def reinitialize_command(
self, command: str | Command, reinit_subcommands=False
) -> Command:
def reinitialize_command(self, command, reinit_subcommands=0):
return self.distribution.reinitialize_command(command, reinit_subcommands)
def run_command(self, command: str) -> None:
def run_command(self, command):
"""Run some other command: uses the 'run_command()' method of
Distribution, which creates and finalizes the command object if
necessary and then invokes its 'run()' method.
"""
self.distribution.run_command(command)
def get_sub_commands(self) -> list[str]:
def get_sub_commands(self):
"""Determine the sub-commands that are relevant in the current
distribution (ie., that need to be run). This is based on the
'sub_commands' class attribute: each tuple in that list may include
@@ -364,57 +326,25 @@ class Command:
run for the current distribution. Return a list of command names.
"""
commands = []
for cmd_name, method in self.sub_commands:
for (cmd_name, method) in self.sub_commands:
if method is None or method(self):
commands.append(cmd_name)
return commands
# -- External world manipulation -----------------------------------
def warn(self, msg: object) -> None:
log.warning("warning: %s: %s\n", self.get_command_name(), msg)
def warn(self, msg):
log.warn("warning: %s: %s\n", self.get_command_name(), msg)
def execute(
self,
func: Callable[[Unpack[_Ts]], object],
args: tuple[Unpack[_Ts]],
msg: object = None,
level: int = 1,
) -> None:
def execute(self, func, args, msg=None, level=1):
util.execute(func, args, msg, dry_run=self.dry_run)
def mkpath(self, name: str, mode: int = 0o777) -> None:
def mkpath(self, name, mode=0o777):
dir_util.mkpath(name, mode, dry_run=self.dry_run)
@overload
def copy_file(
self,
infile: str | os.PathLike[str],
outfile: _StrPathT,
preserve_mode: bool = True,
preserve_times: bool = True,
link: str | None = None,
level: int = 1,
) -> tuple[_StrPathT | str, bool]: ...
@overload
def copy_file(
self,
infile: bytes | os.PathLike[bytes],
outfile: _BytesPathT,
preserve_mode: bool = True,
preserve_times: bool = True,
link: str | None = None,
level: int = 1,
) -> tuple[_BytesPathT | bytes, bool]: ...
def copy_file(
self,
infile: str | os.PathLike[str] | bytes | os.PathLike[bytes],
outfile: str | os.PathLike[str] | bytes | os.PathLike[bytes],
preserve_mode: bool = True,
preserve_times: bool = True,
link: str | None = None,
level: int = 1,
) -> tuple[str | os.PathLike[str] | bytes | os.PathLike[bytes], bool]:
self, infile, outfile, preserve_mode=1, preserve_times=1, link=None, level=1
):
"""Copy a file respecting verbose, dry-run and force flags. (The
former two default to whatever is in the Distribution object, and
the latter defaults to false for commands that don't define it.)"""
@@ -430,13 +360,13 @@ class Command:
def copy_tree(
self,
infile: str | os.PathLike[str],
outfile: str,
preserve_mode: bool = True,
preserve_times: bool = True,
preserve_symlinks: bool = False,
level: int = 1,
) -> list[str]:
infile,
outfile,
preserve_mode=1,
preserve_times=1,
preserve_symlinks=0,
level=1,
):
"""Copy an entire directory tree respecting verbose, dry-run,
and force flags.
"""
@@ -450,60 +380,19 @@ class Command:
dry_run=self.dry_run,
)
@overload
def move_file(
self, src: str | os.PathLike[str], dst: _StrPathT, level: int = 1
) -> _StrPathT | str: ...
@overload
def move_file(
self, src: bytes | os.PathLike[bytes], dst: _BytesPathT, level: int = 1
) -> _BytesPathT | bytes: ...
def move_file(
self,
src: str | os.PathLike[str] | bytes | os.PathLike[bytes],
dst: str | os.PathLike[str] | bytes | os.PathLike[bytes],
level: int = 1,
) -> str | os.PathLike[str] | bytes | os.PathLike[bytes]:
def move_file(self, src, dst, level=1):
"""Move a file respecting dry-run flag."""
return file_util.move_file(src, dst, dry_run=self.dry_run)
def spawn(
self, cmd: MutableSequence[str], search_path: bool = True, level: int = 1
) -> None:
def spawn(self, cmd, search_path=1, level=1):
"""Spawn an external command respecting dry-run flag."""
from distutils.spawn import spawn
spawn(cmd, search_path, dry_run=self.dry_run)
@overload
def make_archive(
self,
base_name: str,
format: str,
root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,
base_dir: str | None = None,
owner: str | None = None,
group: str | None = None,
) -> str: ...
@overload
def make_archive(
self,
base_name: str | os.PathLike[str],
format: str,
root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes],
base_dir: str | None = None,
owner: str | None = None,
group: str | None = None,
) -> str: ...
def make_archive(
self,
base_name: str | os.PathLike[str],
format: str,
root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,
base_dir: str | None = None,
owner: str | None = None,
group: str | None = None,
) -> str:
self, base_name, format, root_dir=None, base_dir=None, owner=None, group=None
):
return archive_util.make_archive(
base_name,
format,
@@ -515,15 +404,8 @@ class Command:
)
def make_file(
self,
infiles: str | list[str] | tuple[str, ...],
outfile: str | os.PathLike[str] | bytes | os.PathLike[bytes],
func: Callable[[Unpack[_Ts]], object],
args: tuple[Unpack[_Ts]],
exec_msg: object = None,
skip_msg: object = None,
level: int = 1,
) -> None:
self, infiles, outfile, func, args, exec_msg=None, skip_msg=None, level=1
):
"""Special case of 'execute()' for operations that process one or
more input files and generate one output file. Works just like
'execute()', except the operation is skipped and a different
@@ -533,7 +415,7 @@ class Command:
timestamp checks.
"""
if skip_msg is None:
skip_msg = f"skipping {outfile} (inputs unchanged)"
skip_msg = "skipping %s (inputs unchanged)" % outfile
# Allow 'infiles' to be a single string
if isinstance(infiles, str):
@@ -547,7 +429,7 @@ class Command:
# If 'outfile' must be regenerated (either because it doesn't
# exist, is out-of-date, or the 'force' flag is true) then
# perform the action that presumably regenerates it
if self.force or _modified.newer_group(infiles, outfile):
if self.force or dep_util.newer_group(infiles, outfile):
self.execute(func, args, exec_msg, level)
# Otherwise, print the "skip" message
else: