up
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user