summaryrefslogtreecommitdiffstats
path: root/python/mach/mach/decorators.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mach/mach/decorators.py')
-rw-r--r--python/mach/mach/decorators.py340
1 files changed, 340 insertions, 0 deletions
diff --git a/python/mach/mach/decorators.py b/python/mach/mach/decorators.py
new file mode 100644
index 0000000000..fe4443e168
--- /dev/null
+++ b/python/mach/mach/decorators.py
@@ -0,0 +1,340 @@
+# 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/.
+
+import argparse
+import collections
+import collections.abc
+from typing import Optional
+
+from mozbuild.base import MachCommandBase
+
+from .base import MachError
+from .registrar import Registrar
+
+
+class _MachCommand(object):
+ """Container for mach command metadata."""
+
+ __slots__ = (
+ # Content from decorator arguments to define the command.
+ "name",
+ "subcommand",
+ "category",
+ "description",
+ "conditions",
+ "_parser",
+ "arguments",
+ "argument_group_names",
+ "virtualenv_name",
+ "ok_if_tests_disabled",
+ # By default, subcommands will be sorted. If this is set to
+ # 'declaration', they will be left in declaration order.
+ "order",
+ # This is the function or callable that will be called when
+ # the command is invoked
+ "func",
+ # The path to the `metrics.yaml` file that describes data that telemetry will
+ # gather for this command. This path is optional.
+ "metrics_path",
+ # Dict of string to _MachCommand defining sub-commands for this
+ # command.
+ "subcommand_handlers",
+ # For subcommands, the global order that the subcommand's declaration
+ # was seen.
+ "decl_order",
+ # Whether to disable automatic logging to last_log.json for the command.
+ "no_auto_log",
+ )
+
+ def __init__(
+ self,
+ name=None,
+ subcommand=None,
+ category=None,
+ description=None,
+ conditions=None,
+ parser=None,
+ order=None,
+ virtualenv_name=None,
+ ok_if_tests_disabled=False,
+ no_auto_log=False,
+ ):
+ 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.virtualenv_name = virtualenv_name
+ self.order = order
+ if ok_if_tests_disabled and category != "testing":
+ raise ValueError(
+ "ok_if_tests_disabled should only be set for " "`testing` mach commands"
+ )
+ self.ok_if_tests_disabled = ok_if_tests_disabled
+
+ self.func = None
+ self.metrics_path = None
+ self.subcommand_handlers = {}
+ self.decl_order = None
+ self.no_auto_log = no_auto_log
+
+ def create_instance(self, context, virtualenv_name):
+ metrics = None
+ if self.metrics_path:
+ metrics = context.telemetry.metrics(self.metrics_path)
+
+ # This ensures the resulting class is defined inside `mach` so that logging
+ # works as expected, and has a meaningful name
+ subclass = type(self.name, (MachCommandBase,), {})
+ return subclass(
+ context,
+ virtualenv_name=virtualenv_name,
+ metrics=metrics,
+ no_auto_log=self.no_auto_log,
+ )
+
+ @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.func.__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 register(self, func):
+ """Register the command in the Registrar with the function to be called on invocation."""
+ if not self.subcommand:
+ if not self.conditions and Registrar.require_conditions:
+ return
+
+ msg = (
+ "Mach command '%s' implemented incorrectly. "
+ + "Conditions argument must take a list "
+ + "of functions. Found %s instead."
+ )
+
+ if not isinstance(self.conditions, collections.abc.Iterable):
+ msg = msg % (self.name, type(self.conditions))
+ raise MachError(msg)
+
+ for c in self.conditions:
+ if not hasattr(c, "__call__"):
+ msg = msg % (self.name, type(c))
+ raise MachError(msg)
+
+ self.func = func
+
+ Registrar.register_command_handler(self)
+
+ else:
+ if self.name not in Registrar.command_handlers:
+ raise MachError(
+ "Command referenced by sub-command does not exist: %s" % self.name
+ )
+
+ self.func = func
+ parent = Registrar.command_handlers[self.name]
+
+ if self.subcommand in parent.subcommand_handlers:
+ raise MachError("sub-command already defined: %s" % self.subcommand)
+
+ parent.subcommand_handlers[self.subcommand] = self
+
+
+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:
+
+ .. code-block:: python
+
+ @Command('foo', category='misc', description='Run the foo action')
+ def foo(self, command_context):
+ pass
+ """
+
+ def __init__(self, name, metrics_path: Optional[str] = None, **kwargs):
+ self._mach_command = _MachCommand(name=name, **kwargs)
+ self._mach_command.metrics_path = metrics_path
+
+ def __call__(self, func):
+ if not hasattr(func, "_mach_command"):
+ func._mach_command = _MachCommand()
+
+ func._mach_command |= self._mach_command
+ func._mach_command.register(func)
+
+ 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.
+ """
+
+ global_order = 0
+
+ def __init__(
+ self,
+ command,
+ subcommand,
+ description=None,
+ parser=None,
+ metrics_path: Optional[str] = None,
+ virtualenv_name: Optional[str] = None,
+ ):
+ self._mach_command = _MachCommand(
+ name=command,
+ subcommand=subcommand,
+ description=description,
+ parser=parser,
+ virtualenv_name=virtualenv_name,
+ )
+ self._mach_command.decl_order = SubCommand.global_order
+ SubCommand.global_order += 1
+
+ self._mach_command.metrics_path = metrics_path
+
+ def __call__(self, func):
+ if not hasattr(func, "_mach_command"):
+ func._mach_command = _MachCommand()
+
+ func._mach_command |= self._mach_command
+ func._mach_command.register(func)
+
+ 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:
+
+ .. code-block:: python
+
+ @Command('foo', help='Run the foo action')
+ @CommandArgument('-b', '--bar', action='store_true', default=False,
+ help='Enable bar mode.')
+ def foo(self, command_context):
+ 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", "metavar") 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:
+
+ .. code-block: python
+
+ @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, command_context):
+ pass
+
+ The name should be chosen so that it makes sense as part of the phrase
+ 'Command Arguments for <name>' 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"
+ "(<section>.<option>, <type_cls>, <description>, <default>, <choices>)\n"
+ "as specified by ConfigSettings._format_metadata."
+ )
+
+ Registrar.register_settings_provider(cls)
+ return cls