python/mach/mach/registrar.py
author Andrew Halberstadt <ahalberstadt@mozilla.com>
Tue, 01 Oct 2019 12:40:28 +0000
changeset 495729 4fbb6d14488034abc1bf7675a30dbbc14f6c004a
parent 490186 d5ef7a5e02a4d12e6349beb11a2cee666ea75b44
permissions -rw-r--r--
Bug 1584567 - [mach] Remove support for conditional_names, r=nalexander Depends on D47626 Differential Revision: https://phabricator.services.mozilla.com/D47627

# 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, print_function, unicode_literals

import time

import six

from .base import MachError

INVALID_COMMAND_CONTEXT = r'''
It looks like you tried to run a mach command from an invalid context. The %s
command failed to meet the following conditions: %s

Run |mach help| to show a list of all commands available to the current context.
'''.lstrip()


class MachRegistrar(object):
    """Container for mach command and config providers."""

    def __init__(self):
        self.command_handlers = {}
        self.commands_by_category = {}
        self.settings_providers = set()
        self.categories = {}
        self.require_conditions = False
        self.command_depth = 0

    def register_command_handler(self, handler):
        name = handler.name

        if not handler.category:
            raise MachError('Cannot register a mach command without a '
                            'category: %s' % name)

        if handler.category not in self.categories:
            raise MachError('Cannot register a command to an undefined '
                            'category: %s -> %s' % (name, handler.category))

        self.command_handlers[name] = handler
        self.commands_by_category[handler.category].add(name)

    def register_settings_provider(self, cls):
        self.settings_providers.add(cls)

    def register_category(self, name, title, description, priority=50):
        self.categories[name] = (title, description, priority)
        self.commands_by_category[name] = set()

    @classmethod
    def _condition_failed_message(cls, name, conditions):
        msg = ['\n']
        for c in conditions:
            part = ['  %s' % c.__name__]
            if c.__doc__ is not None:
                part.append(c.__doc__)
            msg.append(' - '.join(part))
        return INVALID_COMMAND_CONTEXT % (name, '\n'.join(msg))

    @classmethod
    def _instance(_, handler, context, **kwargs):
        cls = handler.cls

        if handler.pass_context and not context:
            raise Exception('mach command class requires context.')

        if context:
            prerun = getattr(context, 'pre_dispatch_handler', None)
            if prerun:
                prerun(context, handler, args=kwargs)

        if handler.pass_context:
            context.handler = handler
            instance = cls(context)
        else:
            instance = cls()

        return instance

    @classmethod
    def _fail_conditions(_, handler, instance):
        fail_conditions = []
        if handler.conditions:
            for c in handler.conditions:
                if not c(instance):
                    fail_conditions.append(c)

        return fail_conditions

    def _run_command_handler(self, handler, context=None, debug_command=False, **kwargs):
        instance = MachRegistrar._instance(handler, context, **kwargs)
        fail_conditions = MachRegistrar._fail_conditions(handler, instance)
        if fail_conditions:
            print(MachRegistrar._condition_failed_message(handler.name, fail_conditions))
            return 1

        self.command_depth += 1
        fn = getattr(instance, handler.method)

        start_time = time.time()

        if debug_command:
            import pdb
            result = pdb.runcall(fn, **kwargs)
        else:
            result = fn(**kwargs)

        end_time = time.time()

        result = result or 0
        assert isinstance(result, six.integer_types)

        if context and not debug_command:
            postrun = getattr(context, 'post_dispatch_handler', None)
            if postrun:
                postrun(context, handler, instance, result,
                        start_time, end_time, self.command_depth, args=kwargs)
        self.command_depth -= 1

        return result

    def dispatch(self, name, context=None, argv=None, subcommand=None, **kwargs):
        """Dispatch/run a command.

        Commands can use this to call other commands.
        """
        handler = self.command_handlers[name]

        if subcommand:
            handler = handler.subcommand_handlers[subcommand]

        if handler.parser:
            parser = handler.parser

            # save and restore existing defaults so **kwargs don't persist across
            # subsequent invocations of Registrar.dispatch()
            old_defaults = parser._defaults.copy()
            parser.set_defaults(**kwargs)
            kwargs, unknown = parser.parse_known_args(argv or [])
            kwargs = vars(kwargs)
            parser._defaults = old_defaults

            if unknown:
                if subcommand:
                    name = '{} {}'.format(name, subcommand)
                parser.error("unrecognized arguments for {}: {}".format(
                    name, ', '.join(["'{}'".format(arg) for arg in unknown])))

        return self._run_command_handler(handler, context=context, **kwargs)


Registrar = MachRegistrar()