codekingpro/portable-devtools
115k
1"""distutils.cmd2 3Provides the Command class, the base class for the command classes4in the distutils.command package.5"""6 7from __future__ import annotations8 9import logging10import os11import re12import sys13from abc import abstractmethod14from collections.abc import Callable, MutableSequence15from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, overload16 17from . import _modified, archive_util, dir_util, file_util, util18from ._log import log19from .errors import DistutilsOptionError20 21if TYPE_CHECKING:22 # type-only import because of mutual dependence between these classes23 from distutils.dist import Distribution24 25 from typing_extensions import TypeVarTuple, Unpack26 27 _Ts = TypeVarTuple("_Ts")28 29_StrPathT = TypeVar("_StrPathT", bound="str | os.PathLike[str]")30_BytesPathT = TypeVar("_BytesPathT", bound="bytes | os.PathLike[bytes]")31_CommandT = TypeVar("_CommandT", bound="Command")32 33 34class Command:35 """Abstract base class for defining command classes, the "worker bees"36 of the Distutils. A useful analogy for command classes is to think of37 them as subroutines with local variables called "options". The options38 are "declared" in 'initialize_options()' and "defined" (given their39 final values, aka "finalized") in 'finalize_options()', both of which40 must be defined by every command class. The distinction between the41 two is necessary because option values might come from the outside42 world (command line, config file, ...), and any options dependent on43 other options must be computed *after* these outside influences have44 been processed -- hence 'finalize_options()'. The "body" of the45 subroutine, where it does all its work based on the values of its46 options, is the 'run()' method, which must also be implemented by every47 command class.48 """49 50 # 'sub_commands' formalizes the notion of a "family" of commands,51 # eg. "install" as the parent with sub-commands "install_lib",52 # "install_headers", etc. The parent of a family of commands53 # defines 'sub_commands' as a class attribute; it's a list of54 # (command_name : string, predicate : unbound_method | string | None)55 # tuples, where 'predicate' is a method of the parent command that56 # determines whether the corresponding command is applicable in the57 # current situation. (Eg. we "install_headers" is only applicable if58 # we have any C header files to install.) If 'predicate' is None,59 # that command is always applicable.60 #61 # 'sub_commands' is usually defined at the *end* of a class, because62 # predicates can be unbound methods, so they must already have been63 # defined. The canonical example is the "install" command.64 sub_commands: ClassVar[ # Any to work around variance issues65 list[tuple[str, Callable[[Any], bool] | None]]66 ] = []67 68 user_options: ClassVar[69 # Specifying both because list is invariant. Avoids mypy override assignment issues70 list[tuple[str, str, str]] | list[tuple[str, str | None, str]]71 ] = []72 73 # -- Creation/initialization methods -------------------------------74 75 def __init__(self, dist: Distribution) -> None:76 """Create and initialize a new Command object. Most importantly,77 invokes the 'initialize_options()' method, which is the real78 initializer and depends on the actual command being79 instantiated.80 """81 # late import because of mutual dependence between these classes82 from distutils.dist import Distribution83 84 if not isinstance(dist, Distribution):85 raise TypeError("dist must be a Distribution instance")86 if self.__class__ is Command:87 raise RuntimeError("Command is an abstract class")88 89 self.distribution = dist90 self.initialize_options()91 92 # Per-command versions of the global flags, so that the user can93 # customize Distutils' behaviour command-by-command and let some94 # commands fall back on the Distribution's behaviour. None means95 # "not defined, check self.distribution's copy".96 97 # verbose is largely ignored, but needs to be set for98 # backwards compatibility (I think)?99 self.verbose = dist.verbose100 101 # Some commands define a 'self.force' option to ignore file102 # timestamps, but methods defined *here* assume that103 # 'self.force' exists for all commands. So define it here104 # just to be safe.105 self.force = None106 107 # The 'help' flag is just used for command-line parsing, so108 # none of that complicated bureaucracy is needed.109 self.help = False110 111 # 'finalized' records whether or not 'finalize_options()' has been112 # called. 'finalize_options()' itself should not pay attention to113 # this flag: it is the business of 'ensure_finalized()', which114 # always calls 'finalize_options()', to respect/update it.115 self.finalized = False116 117 def ensure_finalized(self) -> None:118 if not self.finalized:119 self.finalize_options()120 self.finalized = True121 122 # Subclasses must define:123 # initialize_options()124 # provide default values for all options; may be customized by125 # setup script, by options from config file(s), or by command-line126 # options127 # finalize_options()128 # decide on the final values for all options; this is called129 # after all possible intervention from the outside world130 # (command-line, option file, etc.) has been processed131 # run()132 # run the command: do whatever it is we're here to do,133 # controlled by the command's various option values134 135 @abstractmethod136 def initialize_options(self) -> None:137 """Set default values for all the options that this command138 supports. Note that these defaults may be overridden by other139 commands, by the setup script, by config files, or by the140 command-line. Thus, this is not the place to code dependencies141 between options; generally, 'initialize_options()' implementations142 are just a bunch of "self.foo = None" assignments.143 144 This method must be implemented by all command classes.145 """146 raise RuntimeError(147 f"abstract method -- subclass {self.__class__} must override"148 )149 150 @abstractmethod151 def finalize_options(self) -> None:152 """Set final values for all the options that this command supports.153 This is always called as late as possible, ie. after any option154 assignments from the command-line or from other commands have been155 done. Thus, this is the place to code option dependencies: if156 'foo' depends on 'bar', then it is safe to set 'foo' from 'bar' as157 long as 'foo' still has the same value it was assigned in158 'initialize_options()'.159 160 This method must be implemented by all command classes.161 """162 raise RuntimeError(163 f"abstract method -- subclass {self.__class__} must override"164 )165 166 def dump_options(self, header=None, indent=""):167 from distutils.fancy_getopt import longopt_xlate168 169 if header is None:170 header = f"command options for '{self.get_command_name()}':"171 self.announce(indent + header, level=logging.INFO)172 indent = indent + " "173 for option, _, _ in self.user_options:174 option = option.translate(longopt_xlate)175 if option[-1] == "=":176 option = option[:-1]177 value = getattr(self, option)178 self.announce(indent + f"{option} = {value}", level=logging.INFO)179 180 @abstractmethod181 def run(self) -> None:182 """A command's raison d'etre: carry out the action it exists to183 perform, controlled by the options initialized in184 'initialize_options()', customized by other commands, the setup185 script, the command-line, and config files, and finalized in186 'finalize_options()'. All terminal output and filesystem187 interaction should be done by 'run()'.188 189 This method must be implemented by all command classes.190 """191 raise RuntimeError(192 f"abstract method -- subclass {self.__class__} must override"193 )194 195 def announce(self, msg: object, level: int = logging.DEBUG) -> None:196 log.log(level, msg)197 198 def debug_print(self, msg: object) -> None:199 """Print 'msg' to stdout if the global DEBUG (taken from the200 DISTUTILS_DEBUG environment variable) flag is true.201 """202 from distutils.debug import DEBUG203 204 if DEBUG:205 print(msg)206 sys.stdout.flush()207 208 # -- Option validation methods -------------------------------------209 # (these are very handy in writing the 'finalize_options()' method)210 #211 # NB. the general philosophy here is to ensure that a particular option212 # value meets certain type and value constraints. If not, we try to213 # force it into conformance (eg. if we expect a list but have a string,214 # split the string on comma and/or whitespace). If we can't force the215 # option into conformance, raise DistutilsOptionError. Thus, command216 # classes need do nothing more than (eg.)217 # self.ensure_string_list('foo')218 # and they can be guaranteed that thereafter, self.foo will be219 # a list of strings.220 221 def _ensure_stringlike(self, option, what, default=None):222 val = getattr(self, option)223 if val is None:224 setattr(self, option, default)225 return default226 elif not isinstance(val, str):227 raise DistutilsOptionError(f"'{option}' must be a {what} (got `{val}`)")228 return val229 230 def ensure_string(self, option: str, default: str | None = None) -> None:231 """Ensure that 'option' is a string; if not defined, set it to232 'default'.233 """234 self._ensure_stringlike(option, "string", default)235 236 def ensure_string_list(self, option: str) -> None:237 r"""Ensure that 'option' is a list of strings. If 'option' is238 currently a string, we split it either on /,\s*/ or /\s+/, so239 "foo bar baz", "foo,bar,baz", and "foo, bar baz" all become240 ["foo", "bar", "baz"].241 """242 val = getattr(self, option)243 if val is None:244 return245 elif isinstance(val, str):246 setattr(self, option, re.split(r',\s*|\s+', val))247 else:248 if isinstance(val, list):249 ok = all(isinstance(v, str) for v in val)250 else:251 ok = False252 if not ok:253 raise DistutilsOptionError(254 f"'{option}' must be a list of strings (got {val!r})"255 )256 257 def _ensure_tested_string(self, option, tester, what, error_fmt, default=None):258 val = self._ensure_stringlike(option, what, default)259 if val is not None and not tester(val):260 raise DistutilsOptionError(261 ("error in '%s' option: " + error_fmt) % (option, val)262 )263 264 def ensure_filename(self, option: str) -> None:265 """Ensure that 'option' is the name of an existing file."""266 self._ensure_tested_string(267 option, os.path.isfile, "filename", "'%s' does not exist or is not a file"268 )269 270 def ensure_dirname(self, option: str) -> None:271 self._ensure_tested_string(272 option,273 os.path.isdir,274 "directory name",275 "'%s' does not exist or is not a directory",276 )277 278 # -- Convenience methods for commands ------------------------------279 280 def get_command_name(self) -> str:281 if hasattr(self, 'command_name'):282 return self.command_name283 else:284 return self.__class__.__name__285 286 def set_undefined_options(287 self, src_cmd: str, *option_pairs: tuple[str, str]288 ) -> None:289 """Set the values of any "undefined" options from corresponding290 option values in some other command object. "Undefined" here means291 "is None", which is the convention used to indicate that an option292 has not been changed between 'initialize_options()' and293 'finalize_options()'. Usually called from 'finalize_options()' for294 options that depend on some other command rather than another295 option of the same command. 'src_cmd' is the other command from296 which option values will be taken (a command object will be created297 for it if necessary); the remaining arguments are298 '(src_option,dst_option)' tuples which mean "take the value of299 'src_option' in the 'src_cmd' command object, and copy it to300 'dst_option' in the current command object".301 """302 # Option_pairs: list of (src_option, dst_option) tuples303 src_cmd_obj = self.distribution.get_command_obj(src_cmd)304 src_cmd_obj.ensure_finalized()305 for src_option, dst_option in option_pairs:306 if getattr(self, dst_option) is None:307 setattr(self, dst_option, getattr(src_cmd_obj, src_option))308 309 # NOTE: Because distutils is private to Setuptools and not all commands are exposed here,310 # not every possible command is enumerated in the signature.311 def get_finalized_command(self, command: str, create: bool = True) -> Command:312 """Wrapper around Distribution's 'get_command_obj()' method: find313 (create if necessary and 'create' is true) the command object for314 'command', call its 'ensure_finalized()' method, and return the315 finalized command object.316 """317 cmd_obj = self.distribution.get_command_obj(command, create)318 cmd_obj.ensure_finalized()319 return cmd_obj320 321 # XXX rename to 'get_reinitialized_command()'? (should do the322 # same in dist.py, if so)323 @overload324 def reinitialize_command(325 self, command: str, reinit_subcommands: bool = False326 ) -> Command: ...327 @overload328 def reinitialize_command(329 self, command: _CommandT, reinit_subcommands: bool = False330 ) -> _CommandT: ...331 def reinitialize_command(332 self, command: str | Command, reinit_subcommands=False333 ) -> Command:334 return self.distribution.reinitialize_command(command, reinit_subcommands)335 336 def run_command(self, command: str) -> None:337 """Run some other command: uses the 'run_command()' method of338 Distribution, which creates and finalizes the command object if339 necessary and then invokes its 'run()' method.340 """341 self.distribution.run_command(command)342 343 def get_sub_commands(self) -> list[str]:344 """Determine the sub-commands that are relevant in the current345 distribution (ie., that need to be run). This is based on the346 'sub_commands' class attribute: each tuple in that list may include347 a method that we call to determine if the subcommand needs to be348 run for the current distribution. Return a list of command names.349 """350 commands = []351 for cmd_name, method in self.sub_commands:352 if method is None or method(self):353 commands.append(cmd_name)354 return commands355 356 # -- External world manipulation -----------------------------------357 358 def warn(self, msg: object) -> None:359 log.warning("warning: %s: %s\n", self.get_command_name(), msg)360 361 def execute(362 self,363 func: Callable[[Unpack[_Ts]], object],364 args: tuple[Unpack[_Ts]],365 msg: object = None,366 level: int = 1,367 ) -> None:368 util.execute(func, args, msg)369 370 def mkpath(self, name: str, mode: int = 0o777) -> None:371 dir_util.mkpath(name, mode)372 373 @overload374 def copy_file(375 self,376 infile: str | os.PathLike[str],377 outfile: _StrPathT,378 preserve_mode: bool = True,379 preserve_times: bool = True,380 link: str | None = None,381 level: int = 1,382 ) -> tuple[_StrPathT | str, bool]: ...383 @overload384 def copy_file(385 self,386 infile: bytes | os.PathLike[bytes],387 outfile: _BytesPathT,388 preserve_mode: bool = True,389 preserve_times: bool = True,390 link: str | None = None,391 level: int = 1,392 ) -> tuple[_BytesPathT | bytes, bool]: ...393 def copy_file(394 self,395 infile: str | os.PathLike[str] | bytes | os.PathLike[bytes],396 outfile: str | os.PathLike[str] | bytes | os.PathLike[bytes],397 preserve_mode: bool = True,398 preserve_times: bool = True,399 link: str | None = None,400 level: int = 1,401 ) -> tuple[str | os.PathLike[str] | bytes | os.PathLike[bytes], bool]:402 """Copy a file respecting verbose, dry-run and force flags. (The403 former two default to whatever is in the Distribution object, and404 the latter defaults to false for commands that don't define it.)"""405 return file_util.copy_file(406 infile,407 outfile,408 preserve_mode,409 preserve_times,410 not self.force,411 link,412 )413 414 def copy_tree(415 self,416 infile: str | os.PathLike[str],417 outfile: str,418 preserve_mode: bool = True,419 preserve_times: bool = True,420 preserve_symlinks: bool = False,421 level: int = 1,422 ) -> list[str]:423 """Copy an entire directory tree respecting verbose, dry-run,424 and force flags.425 """426 return dir_util.copy_tree(427 infile,428 outfile,429 preserve_mode,430 preserve_times,431 preserve_symlinks,432 not self.force,433 )434 435 @overload436 def move_file(437 self, src: str | os.PathLike[str], dst: _StrPathT, level: int = 1438 ) -> _StrPathT | str: ...439 @overload440 def move_file(441 self, src: bytes | os.PathLike[bytes], dst: _BytesPathT, level: int = 1442 ) -> _BytesPathT | bytes: ...443 def move_file(444 self,445 src: str | os.PathLike[str] | bytes | os.PathLike[bytes],446 dst: str | os.PathLike[str] | bytes | os.PathLike[bytes],447 level: int = 1,448 ) -> str | os.PathLike[str] | bytes | os.PathLike[bytes]:449 """Move a file respecting dry-run flag."""450 return file_util.move_file(src, dst)451 452 def spawn(453 self, cmd: MutableSequence[str], search_path: bool = True, level: int = 1454 ) -> None:455 """Spawn an external command respecting dry-run flag."""456 from distutils.spawn import spawn457 458 spawn(cmd, search_path)459 460 @overload461 def make_archive(462 self,463 base_name: str,464 format: str,465 root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,466 base_dir: str | None = None,467 owner: str | None = None,468 group: str | None = None,469 ) -> str: ...470 @overload471 def make_archive(472 self,473 base_name: str | os.PathLike[str],474 format: str,475 root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes],476 base_dir: str | None = None,477 owner: str | None = None,478 group: str | None = None,479 ) -> str: ...480 def make_archive(481 self,482 base_name: str | os.PathLike[str],483 format: str,484 root_dir: str | os.PathLike[str] | bytes | os.PathLike[bytes] | None = None,485 base_dir: str | None = None,486 owner: str | None = None,487 group: str | None = None,488 ) -> str:489 return archive_util.make_archive(490 base_name,491 format,492 root_dir,493 base_dir,494 owner=owner,495 group=group,496 )497 498 def make_file(499 self,500 infiles: str | list[str] | tuple[str, ...],501 outfile: str | os.PathLike[str] | bytes | os.PathLike[bytes],502 func: Callable[[Unpack[_Ts]], object],503 args: tuple[Unpack[_Ts]],504 exec_msg: object = None,505 skip_msg: object = None,506 level: int = 1,507 ) -> None:508 """Special case of 'execute()' for operations that process one or509 more input files and generate one output file. Works just like510 'execute()', except the operation is skipped and a different511 message printed if 'outfile' already exists and is newer than all512 files listed in 'infiles'. If the command defined 'self.force',513 and it is true, then the command is unconditionally run -- does no514 timestamp checks.515 """516 if skip_msg is None:517 skip_msg = f"skipping {outfile} (inputs unchanged)"518 519 # Allow 'infiles' to be a single string520 if isinstance(infiles, str):521 infiles = (infiles,)522 elif not isinstance(infiles, (list, tuple)):523 raise TypeError("'infiles' must be a string, or a list or tuple of strings")524 525 if exec_msg is None:526 exec_msg = "generating {} from {}".format(outfile, ', '.join(infiles))527 528 # If 'outfile' must be regenerated (either because it doesn't529 # exist, is out-of-date, or the 'force' flag is true) then530 # perform the action that presumably regenerates it531 if self.force or _modified.newer_group(infiles, outfile):532 self.execute(func, args, exec_msg, level)533 # Otherwise, print the "skip" message534 else:535 log.debug(skip_msg)536 