From f4de057784291f9619191f22b386d9b699507667 Mon Sep 17 00:00:00 2001 From: Josh Matthews Date: Tue, 21 Dec 2021 10:57:18 -0500 Subject: [PATCH] Vendor mach-1.0.0. --- python/mach/PKG-INFO | 29 + python/mach/README.rst | 13 + python/mach/mach.egg-info/PKG-INFO | 29 + python/mach/mach.egg-info/SOURCES.txt | 21 + .../mach/mach.egg-info/dependency_links.txt | 1 + python/mach/mach.egg-info/requires.txt | 4 + python/mach/mach.egg-info/top_level.txt | 1 + python/mach/mach/__init__.py | 0 python/mach/mach/base.py | 66 ++ python/mach/mach/config.py | 419 ++++++++++++ python/mach/mach/decorators.py | 348 ++++++++++ python/mach/mach/dispatcher.py | 468 +++++++++++++ python/mach/mach/logging.py | 298 +++++++++ python/mach/mach/main.py | 613 ++++++++++++++++++ python/mach/mach/mixin/__init__.py | 0 python/mach/mach/mixin/logging.py | 54 ++ python/mach/mach/mixin/process.py | 176 +++++ python/mach/mach/registrar.py | 155 +++++ python/mach/mach/terminal.py | 76 +++ python/mach/mach/util.py | 30 + python/mach/setup.cfg | 7 + python/mach/setup.py | 41 ++ python/mach_bootstrap.py | 1 + python/requirements.txt | 1 - 24 files changed, 2850 insertions(+), 1 deletion(-) create mode 100644 python/mach/PKG-INFO create mode 100644 python/mach/README.rst create mode 100644 python/mach/mach.egg-info/PKG-INFO create mode 100644 python/mach/mach.egg-info/SOURCES.txt create mode 100644 python/mach/mach.egg-info/dependency_links.txt create mode 100644 python/mach/mach.egg-info/requires.txt create mode 100644 python/mach/mach.egg-info/top_level.txt create mode 100644 python/mach/mach/__init__.py create mode 100644 python/mach/mach/base.py create mode 100644 python/mach/mach/config.py create mode 100644 python/mach/mach/decorators.py create mode 100644 python/mach/mach/dispatcher.py create mode 100644 python/mach/mach/logging.py create mode 100644 python/mach/mach/main.py create mode 100644 python/mach/mach/mixin/__init__.py create mode 100644 python/mach/mach/mixin/logging.py create mode 100644 python/mach/mach/mixin/process.py create mode 100644 python/mach/mach/registrar.py create mode 100644 python/mach/mach/terminal.py create mode 100644 python/mach/mach/util.py create mode 100644 python/mach/setup.cfg create mode 100644 python/mach/setup.py diff --git a/python/mach/PKG-INFO b/python/mach/PKG-INFO new file mode 100644 index 000000000000..8f70511a6887 --- /dev/null +++ b/python/mach/PKG-INFO @@ -0,0 +1,29 @@ +Metadata-Version: 1.1 +Name: mach +Version: 1.0.0 +Summary: Generic command line command dispatching framework. +Home-page: https://developer.mozilla.org/en-US/docs/Developer_Guide/mach +Author: Gregory Szorc +Author-email: gregory.szorc@gmail.com +License: MPL 2.0 +Description: ==== + mach + ==== + + Mach (German for *do*) is a generic command dispatcher for the command + line. + + To use mach, you install the mach core (a Python package), create an + executable *driver* script (named whatever you want), and write mach + commands. When the *driver* is executed, mach dispatches to the + requested command handler automatically. + + To learn more, read the docs in ``docs/``. + +Platform: UNKNOWN +Classifier: Environment :: Console +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) +Classifier: Natural Language :: English +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.5 diff --git a/python/mach/README.rst b/python/mach/README.rst new file mode 100644 index 000000000000..7c2e00becbad --- /dev/null +++ b/python/mach/README.rst @@ -0,0 +1,13 @@ +==== +mach +==== + +Mach (German for *do*) is a generic command dispatcher for the command +line. + +To use mach, you install the mach core (a Python package), create an +executable *driver* script (named whatever you want), and write mach +commands. When the *driver* is executed, mach dispatches to the +requested command handler automatically. + +To learn more, read the docs in ``docs/``. diff --git a/python/mach/mach.egg-info/PKG-INFO b/python/mach/mach.egg-info/PKG-INFO new file mode 100644 index 000000000000..9cd0d23d6102 --- /dev/null +++ b/python/mach/mach.egg-info/PKG-INFO @@ -0,0 +1,29 @@ +Metadata-Version: 1.1 +Name: mach +Version: 1.0.1 +Summary: Generic command line command dispatching framework. +Home-page: https://developer.mozilla.org/en-US/docs/Developer_Guide/mach +Author: Gregory Szorc +Author-email: gregory.szorc@gmail.com +License: MPL 2.0 +Description: ==== + mach + ==== + + Mach (German for *do*) is a generic command dispatcher for the command + line. + + To use mach, you install the mach core (a Python package), create an + executable *driver* script (named whatever you want), and write mach + commands. When the *driver* is executed, mach dispatches to the + requested command handler automatically. + + To learn more, read the docs in ``docs/``. + +Platform: UNKNOWN +Classifier: Environment :: Console +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) +Classifier: Natural Language :: English +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.5 diff --git a/python/mach/mach.egg-info/SOURCES.txt b/python/mach/mach.egg-info/SOURCES.txt new file mode 100644 index 000000000000..85848a51a506 --- /dev/null +++ b/python/mach/mach.egg-info/SOURCES.txt @@ -0,0 +1,21 @@ +README.rst +setup.cfg +setup.py +mach/__init__.py +mach/base.py +mach/config.py +mach/decorators.py +mach/dispatcher.py +mach/logging.py +mach/main.py +mach/registrar.py +mach/terminal.py +mach/util.py +mach.egg-info/PKG-INFO +mach.egg-info/SOURCES.txt +mach.egg-info/dependency_links.txt +mach.egg-info/requires.txt +mach.egg-info/top_level.txt +mach/mixin/__init__.py +mach/mixin/logging.py +mach/mixin/process.py \ No newline at end of file diff --git a/python/mach/mach.egg-info/dependency_links.txt b/python/mach/mach.egg-info/dependency_links.txt new file mode 100644 index 000000000000..8b137891791f --- /dev/null +++ b/python/mach/mach.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/python/mach/mach.egg-info/requires.txt b/python/mach/mach.egg-info/requires.txt new file mode 100644 index 000000000000..b2377dbc846b --- /dev/null +++ b/python/mach/mach.egg-info/requires.txt @@ -0,0 +1,4 @@ +blessings +mozfile +mozprocess +six diff --git a/python/mach/mach.egg-info/top_level.txt b/python/mach/mach.egg-info/top_level.txt new file mode 100644 index 000000000000..fc6ac35d8393 --- /dev/null +++ b/python/mach/mach.egg-info/top_level.txt @@ -0,0 +1 @@ +mach diff --git a/python/mach/mach/__init__.py b/python/mach/mach/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/python/mach/mach/base.py b/python/mach/mach/base.py new file mode 100644 index 000000000000..4748d2dcc60f --- /dev/null +++ b/python/mach/mach/base.py @@ -0,0 +1,66 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, unicode_literals + + +class CommandContext(object): + """Holds run-time state so it can easily be passed to command providers.""" + def __init__(self, cwd=None, settings=None, log_manager=None, commands=None, **kwargs): + self.cwd = cwd + self.settings = settings + self.log_manager = log_manager + self.commands = commands + self.command_attrs = {} + + for k, v in kwargs.items(): + setattr(self, k, v) + + +class MachError(Exception): + """Base class for all errors raised by mach itself.""" + + +class NoCommandError(MachError): + """No command was passed into mach.""" + + +class UnknownCommandError(MachError): + """Raised when we attempted to execute an unknown command.""" + + def __init__(self, command, verb, suggested_commands=None): + MachError.__init__(self) + + self.command = command + self.verb = verb + self.suggested_commands = suggested_commands or [] + + +class UnrecognizedArgumentError(MachError): + """Raised when an unknown argument is passed to mach.""" + + def __init__(self, command, arguments): + MachError.__init__(self) + + self.command = command + self.arguments = arguments + + +class FailedCommandError(Exception): + """Raised by commands to signal a handled failure to be printed by mach + + When caught by mach a FailedCommandError will print message and exit + with ''exit_code''. The optional ''reason'' is a string in cases where + other scripts may wish to handle the exception, though this is generally + intended to communicate failure to mach. + """ + + def __init__(self, message, exit_code=1, reason=''): + Exception.__init__(self, message) + self.exit_code = exit_code + self.reason = reason + + +class MissingFileError(MachError): + """Attempted to load a mach commands file that doesn't exist.""" diff --git a/python/mach/mach/config.py b/python/mach/mach/config.py new file mode 100644 index 000000000000..7210eca82308 --- /dev/null +++ b/python/mach/mach/config.py @@ -0,0 +1,419 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +r""" +This file defines classes for representing config data/settings. + +Config data is modeled as key-value pairs. Keys are grouped together into named +sections. Individual config settings (options) have metadata associated with +them. This metadata includes type, default value, valid values, etc. + +The main interface to config data is the ConfigSettings class. 1 or more +ConfigProvider classes are associated with ConfigSettings and define what +settings are available. +""" + +from __future__ import absolute_import, unicode_literals + +import collections +import os +import sys +import six +from functools import wraps +from six.moves.configparser import RawConfigParser, NoSectionError +from six import string_types + + +class ConfigException(Exception): + pass + + +class ConfigType(object): + """Abstract base class for config values.""" + + @staticmethod + def validate(value): + """Validates a Python value conforms to this type. + + Raises a TypeError or ValueError if it doesn't conform. Does not do + anything if the value is valid. + """ + + @staticmethod + def from_config(config, section, option): + """Obtain the value of this type from a RawConfigParser. + + Receives a RawConfigParser instance, a str section name, and the str + option in that section to retrieve. + + The implementation may assume the option exists in the RawConfigParser + instance. + + Implementations are not expected to validate the value. But, they + should return the appropriate Python type. + """ + + @staticmethod + def to_config(value): + return value + + +class StringType(ConfigType): + @staticmethod + def validate(value): + if not isinstance(value, string_types): + raise TypeError() + + @staticmethod + def from_config(config, section, option): + return config.get(section, option) + + +class BooleanType(ConfigType): + @staticmethod + def validate(value): + if not isinstance(value, bool): + raise TypeError() + + @staticmethod + def from_config(config, section, option): + return config.getboolean(section, option) + + @staticmethod + def to_config(value): + return 'true' if value else 'false' + + +class IntegerType(ConfigType): + @staticmethod + def validate(value): + if not isinstance(value, int): + raise TypeError() + + @staticmethod + def from_config(config, section, option): + return config.getint(section, option) + + +class PositiveIntegerType(IntegerType): + @staticmethod + def validate(value): + if not isinstance(value, int): + raise TypeError() + + if value < 0: + raise ValueError() + + +class PathType(StringType): + @staticmethod + def validate(value): + if not isinstance(value, string_types): + raise TypeError() + + @staticmethod + def from_config(config, section, option): + return config.get(section, option) + + +TYPE_CLASSES = { + 'string': StringType, + 'boolean': BooleanType, + 'int': IntegerType, + 'pos_int': PositiveIntegerType, + 'path': PathType, +} + + +class DefaultValue(object): + pass + + +def reraise_attribute_error(func): + """Used to make sure __getattr__ wrappers around __getitem__ + raise AttributeError instead of KeyError. + """ + @wraps(func) + def _(*args, **kwargs): + try: + return func(*args, **kwargs) + except KeyError: + exc_class, exc, tb = sys.exc_info() + six.reraise(AttributeError().__class__, exc, tb) + return _ + + +class ConfigSettings(collections.Mapping): + """Interface for configuration settings. + + This is the main interface to the configuration. + + A configuration is a collection of sections. Each section contains + key-value pairs. + + When an instance is created, the caller first registers ConfigProvider + instances with it. This tells the ConfigSettings what individual settings + are available and defines extra metadata associated with those settings. + This is used for validation, etc. + + Once ConfigProvider instances are registered, a config is populated. It can + be loaded from files or populated by hand. + + ConfigSettings instances are accessed like dictionaries or by using + attributes. e.g. the section "foo" is accessed through either + settings.foo or settings['foo']. + + Sections are modeled by the ConfigSection class which is defined inside + this one. They look just like dicts or classes with attributes. To access + the "bar" option in the "foo" section: + + value = settings.foo.bar + value = settings['foo']['bar'] + value = settings.foo['bar'] + + Assignment is similar: + + settings.foo.bar = value + settings['foo']['bar'] = value + settings['foo'].bar = value + + You can even delete user-assigned values: + + del settings.foo.bar + del settings['foo']['bar'] + + If there is a default, it will be returned. + + When settings are mutated, they are validated against the registered + providers. Setting unknown settings or setting values to illegal values + will result in exceptions being raised. + """ + + class ConfigSection(collections.MutableMapping, object): + """Represents an individual config section.""" + def __init__(self, config, name, settings): + object.__setattr__(self, '_config', config) + object.__setattr__(self, '_name', name) + object.__setattr__(self, '_settings', settings) + + wildcard = any(s == '*' for s in self._settings) + object.__setattr__(self, '_wildcard', wildcard) + + @property + def options(self): + try: + return self._config.options(self._name) + except NoSectionError: + return [] + + def get_meta(self, option): + if option in self._settings: + return self._settings[option] + if self._wildcard: + return self._settings['*'] + raise KeyError('Option not registered with provider: %s' % option) + + def _validate(self, option, value): + meta = self.get_meta(option) + meta['type_cls'].validate(value) + + if 'choices' in meta and value not in meta['choices']: + raise ValueError("Value '%s' must be one of: %s" % ( + value, ', '.join(sorted(meta['choices'])))) + + # MutableMapping interface + def __len__(self): + return len(self.options) + + def __iter__(self): + return iter(self.options) + + def __contains__(self, k): + return self._config.has_option(self._name, k) + + def __getitem__(self, k): + meta = self.get_meta(k) + + if self._config.has_option(self._name, k): + v = meta['type_cls'].from_config(self._config, self._name, k) + else: + v = meta.get('default', DefaultValue) + + if v == DefaultValue: + raise KeyError('No default value registered: %s' % k) + + self._validate(k, v) + return v + + def __setitem__(self, k, v): + self._validate(k, v) + meta = self.get_meta(k) + + if not self._config.has_section(self._name): + self._config.add_section(self._name) + + self._config.set(self._name, k, meta['type_cls'].to_config(v)) + + def __delitem__(self, k): + self._config.remove_option(self._name, k) + + # Prune empty sections. + if not len(self._config.options(self._name)): + self._config.remove_section(self._name) + + @reraise_attribute_error + def __getattr__(self, k): + return self.__getitem__(k) + + @reraise_attribute_error + def __setattr__(self, k, v): + self.__setitem__(k, v) + + @reraise_attribute_error + def __delattr__(self, k): + self.__delitem__(k) + + def __init__(self): + self._config = RawConfigParser() + self._config.optionxform = str + + self._settings = {} + self._sections = {} + self._finalized = False + self.loaded_files = set() + + def load_file(self, filename): + self.load_files([filename]) + + def load_files(self, filenames): + """Load a config from files specified by their paths. + + Files are loaded in the order given. Subsequent files will overwrite + values from previous files. If a file does not exist, it will be + ignored. + """ + filtered = [f for f in filenames if os.path.exists(f)] + + fps = [open(f, 'rt') for f in filtered] + self.load_fps(fps) + self.loaded_files.update(set(filtered)) + for fp in fps: + fp.close() + + def load_fps(self, fps): + """Load config data by reading file objects.""" + + for fp in fps: + self._config.readfp(fp) + + def write(self, fh): + """Write the config to a file object.""" + self._config.write(fh) + + @classmethod + def _format_metadata(cls, provider, section, option, type_cls, description, + default=DefaultValue, extra=None): + """Formats and returns the metadata for a setting. + + Each setting must have: + + section -- str section to which the setting belongs. This is how + settings are grouped. + + option -- str id for the setting. This must be unique within the + section it appears. + + type -- a ConfigType-derived type defining the type of the setting. + + description -- str describing how to use the setting and where it + applies. + + Each setting has the following optional parameters: + + default -- The default value for the setting. If None (the default) + there is no default. + + extra -- A dict of additional key/value pairs to add to the + setting metadata. + """ + if isinstance(type_cls, string_types): + type_cls = TYPE_CLASSES[type_cls] + + meta = { + 'description': description, + 'type_cls': type_cls, + } + + if default != DefaultValue: + meta['default'] = default + + if extra: + meta.update(extra) + + return meta + + def register_provider(self, provider): + """Register a SettingsProvider with this settings interface.""" + + if self._finalized: + raise ConfigException('Providers cannot be registered after finalized.') + + settings = provider.config_settings + if callable(settings): + settings = settings() + + config_settings = collections.defaultdict(dict) + for setting in settings: + section, option = setting[0].split('.') + + if option in config_settings[section]: + raise ConfigException('Setting has already been registered: %s.%s' % ( + section, option)) + + meta = self._format_metadata(provider, section, option, *setting[1:]) + config_settings[section][option] = meta + + for section_name, settings in config_settings.items(): + section = self._settings.get(section_name, {}) + + for k, v in settings.items(): + if k in section: + raise ConfigException('Setting already registered: %s.%s' % + (section_name, k)) + + section[k] = v + + self._settings[section_name] = section + + def _finalize(self): + if self._finalized: + return + + for section, settings in self._settings.items(): + s = ConfigSettings.ConfigSection(self._config, section, settings) + self._sections[section] = s + + self._finalized = True + + # Mapping interface. + def __len__(self): + return len(self._settings) + + def __iter__(self): + self._finalize() + + return iter(self._sections.keys()) + + def __contains__(self, k): + return k in self._settings + + def __getitem__(self, k): + self._finalize() + + return self._sections[k] + + # Allow attribute access because it looks nice. + @reraise_attribute_error + def __getattr__(self, k): + return self.__getitem__(k) diff --git a/python/mach/mach/decorators.py b/python/mach/mach/decorators.py new file mode 100644 index 000000000000..f73af846d4bb --- /dev/null +++ b/python/mach/mach/decorators.py @@ -0,0 +1,348 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from __future__ import absolute_import, unicode_literals + +import argparse +import collections +import inspect +import sys +import types + +from .base import MachError +from .registrar import Registrar + + +class _MachCommand(object): + """Container for mach command metadata. + + Mach commands contain lots of attributes. This class exists to capture them + in a sane way so tuples, etc aren't used instead. + """ + __slots__ = ( + # Content from decorator arguments to define the command. + 'name', + 'subcommand', + 'category', + 'description', + 'conditions', + '_parser', + 'arguments', + 'argument_group_names', + + # Describes how dispatch is performed. + + # The Python class providing the command. This is the class type not + # an instance of the class. Mach will instantiate a new instance of + # the class if the command is executed. + 'cls', + + # Whether the __init__ method of the class should receive a mach + # context instance. This should only affect the mach driver and how + # it instantiates classes. + 'pass_context', + + # The name of the method providing the command. In other words, this + # is the str name of the attribute on the class type corresponding to + # the name of the function. + 'method', + + # Dict of string to _MachCommand defining sub-commands for this + # command. + 'subcommand_handlers', + ) + + def __init__(self, name=None, subcommand=None, category=None, + description=None, conditions=None, parser=None): + self.name = name + self.subcommand = subcommand + self.category = category + self.description = description + self.conditions = conditions or [] + self._parser = parser + self.arguments = [] + self.argument_group_names = [] + + self.cls = None + self.pass_context = None + self.method = None + self.subcommand_handlers = {} + + @property + def parser(self): + # Creating CLI parsers at command dispatch time can be expensive. Make + # it possible to lazy load them by using functions. + if callable(self._parser): + self._parser = self._parser() + + return self._parser + + @property + def docstring(self): + return self.cls.__dict__[self.method].__doc__ + + def __ior__(self, other): + if not isinstance(other, _MachCommand): + raise ValueError('can only operate on _MachCommand instances') + + for a in self.__slots__: + if not getattr(self, a): + setattr(self, a, getattr(other, a)) + + return self + + +def CommandProvider(cls): + """Class decorator to denote that it provides subcommands for Mach. + + When this decorator is present, mach looks for commands being defined by + methods inside the class. + """ + + # The implementation of this decorator relies on the parse-time behavior of + # decorators. When the module is imported, the method decorators (like + # @Command and @CommandArgument) are called *before* this class decorator. + # The side-effect of the method decorators is to store specifically-named + # attributes on the function types. We just scan over all functions in the + # class looking for the side-effects of the method decorators. + + # Tell mach driver whether to pass context argument to __init__. + pass_context = False + + isfunc = inspect.ismethod if sys.version_info < (3, 0) else inspect.isfunction + if isfunc(cls.__init__): + spec = inspect.getargspec(cls.__init__) + + if len(spec.args) > 2: + msg = 'Mach @CommandProvider class %s implemented incorrectly. ' + \ + '__init__() must take 1 or 2 arguments. From %s' + msg = msg % (cls.__name__, inspect.getsourcefile(cls)) + raise MachError(msg) + + if len(spec.args) == 2: + pass_context = True + + seen_commands = set() + + # We scan __dict__ because we only care about the classes own attributes, + # not inherited ones. If we did inherited attributes, we could potentially + # define commands multiple times. We also sort keys so commands defined in + # the same class are grouped in a sane order. + for attr in sorted(cls.__dict__.keys()): + value = cls.__dict__[attr] + + if not isinstance(value, types.FunctionType): + continue + + command = getattr(value, '_mach_command', None) + if not command: + continue + + # Ignore subcommands for now: we handle them later. + if command.subcommand: + continue + + seen_commands.add(command.name) + + if not command.conditions and Registrar.require_conditions: + continue + + msg = 'Mach command \'%s\' implemented incorrectly. ' + \ + 'Conditions argument must take a list ' + \ + 'of functions. Found %s instead.' + + if not isinstance(command.conditions, collections.Iterable): + msg = msg % (command.name, type(command.conditions)) + raise MachError(msg) + + for c in command.conditions: + if not hasattr(c, '__call__'): + msg = msg % (command.name, type(c)) + raise MachError(msg) + + command.cls = cls + command.method = attr + command.pass_context = pass_context + + Registrar.register_command_handler(command) + + # Now do another pass to get sub-commands. We do this in two passes so + # we can check the parent command existence without having to hold + # state and reconcile after traversal. + for attr in sorted(cls.__dict__.keys()): + value = cls.__dict__[attr] + + if not isinstance(value, types.FunctionType): + continue + + command = getattr(value, '_mach_command', None) + if not command: + continue + + # It is a regular command. + if not command.subcommand: + continue + + if command.name not in seen_commands: + raise MachError('Command referenced by sub-command does not ' + 'exist: %s' % command.name) + + if command.name not in Registrar.command_handlers: + continue + + command.cls = cls + command.method = attr + command.pass_context = pass_context + parent = Registrar.command_handlers[command.name] + + if command.subcommand in parent.subcommand_handlers: + raise MachError('sub-command already defined: %s' % command.subcommand) + + parent.subcommand_handlers[command.subcommand] = command + + return cls + + +class Command(object): + """Decorator for functions or methods that provide a mach command. + + The decorator accepts arguments that define basic attributes of the + command. The following arguments are recognized: + + category -- The string category to which this command belongs. Mach's + help will group commands by category. + + description -- A brief description of what the command does. + + parser -- an optional argparse.ArgumentParser instance or callable + that returns an argparse.ArgumentParser instance to use as the + basis for the command arguments. + + For example: + + @Command('foo', category='misc', description='Run the foo action') + def foo(self): + pass + """ + def __init__(self, name, **kwargs): + self._mach_command = _MachCommand(name=name, **kwargs) + + def __call__(self, func): + if not hasattr(func, '_mach_command'): + func._mach_command = _MachCommand() + + func._mach_command |= self._mach_command + + return func + + +class SubCommand(object): + """Decorator for functions or methods that provide a sub-command. + + Mach commands can have sub-commands. e.g. ``mach command foo`` or + ``mach command bar``. Each sub-command has its own parser and is + effectively its own mach command. + + The decorator accepts arguments that define basic attributes of the + sub command: + + command -- The string of the command this sub command should be + attached to. + + subcommand -- The string name of the sub command to register. + + description -- A textual description for this sub command. + """ + def __init__(self, command, subcommand, description=None, parser=None): + self._mach_command = _MachCommand(name=command, subcommand=subcommand, + description=description, parser=parser) + + def __call__(self, func): + if not hasattr(func, '_mach_command'): + func._mach_command = _MachCommand() + + func._mach_command |= self._mach_command + + return func + + +class CommandArgument(object): + """Decorator for additional arguments to mach subcommands. + + This decorator should be used to add arguments to mach commands. Arguments + to the decorator are proxied to ArgumentParser.add_argument(). + + For example: + + @Command('foo', help='Run the foo action') + @CommandArgument('-b', '--bar', action='store_true', default=False, + help='Enable bar mode.') + def foo(self): + pass + """ + def __init__(self, *args, **kwargs): + if kwargs.get('nargs') == argparse.REMAINDER: + # These are the assertions we make in dispatcher.py about + # those types of CommandArguments. + assert len(args) == 1 + assert all(k in ('default', 'nargs', 'help', 'group') for k in kwargs) + self._command_args = (args, kwargs) + + def __call__(self, func): + if not hasattr(func, '_mach_command'): + func._mach_command = _MachCommand() + + func._mach_command.arguments.insert(0, self._command_args) + + return func + + +class CommandArgumentGroup(object): + """Decorator for additional argument groups to mach commands. + + This decorator should be used to add arguments groups to mach commands. + Arguments to the decorator are proxied to + ArgumentParser.add_argument_group(). + + For example: + + @Command('foo', helps='Run the foo action') + @CommandArgumentGroup('group1') + @CommandArgument('-b', '--bar', group='group1', action='store_true', + default=False, help='Enable bar mode.') + def foo(self): + pass + + The name should be chosen so that it makes sense as part of the phrase + 'Command Arguments for ' because that's how it will be shown in the + help message. + """ + def __init__(self, group_name): + self._group_name = group_name + + def __call__(self, func): + if not hasattr(func, '_mach_command'): + func._mach_command = _MachCommand() + + func._mach_command.argument_group_names.insert(0, self._group_name) + + return func + + +def SettingsProvider(cls): + """Class decorator to denote that this class provides Mach settings. + + When this decorator is encountered, the underlying class will automatically + be registered with the Mach registrar and will (likely) be hooked up to the + mach driver. + """ + if not hasattr(cls, 'config_settings'): + raise MachError('@SettingsProvider must contain a config_settings attribute. It ' + 'may either be a list of tuples, or a callable that returns a list ' + 'of tuples. Each tuple must be of the form:\n' + '(
.