Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes15kdownloads
cmd.py536 linesDownload Raw Back to _distutils
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 
codekingpro/portable-devtools · Team Ai