Skip to content
Snippets Groups Projects
acmdlib.py 5.57 KiB
Newer Older
# Copyright (C) 2002-2019 CERN for the benefit of the ATLAS collaboration

# @file PyUtils.acmdlib
# @purpose a library to ease the writing of sub-command scripts
# @author Sebastien Binet
# @date January 2010

from __future__ import with_statement

__version__ = "$Revision: 464077 $"
__doc__ = "a library to ease the writing of sub-command scripts"
__author__ = "Sebastien Binet"

__all__ = [
    'Command',
    'command',
    'argument',
    'register',
    ]

### imports -------------------------------------------------------------------
import argparse
import textwrap

### globals -------------------------------------------------------------------
ACMD_GROUPNAME = 'acmdlib.commands'
"""The name under which all commands are grouped
"""

ACMD_PARSER = argparse.ArgumentParser(
    prog="acmd",
    description="a general script interface with sub-commands",
    )
ACMD_PARSER.add_argument(
    '--version',
    action='version',
    version=__version__,
    help="show program's version number and exit")

ACMD_SUBPARSERS = ACMD_PARSER.add_subparsers(
    dest='command',
    title='commands',
    metavar='COMMAND',
    )

### classes -------------------------------------------------------------------
class Command(object):
    """A wrapper class to manage the creation of commands and their arguments

    this is very heavily inspired from:
    http://pypi.python.org/pypi/django-boss (MIT licence)
    """

    def __init__(self, fct, **kwargs):
        object.__init__(self)
        self.fct = fct
        self.parser = self._make_parser(**kwargs)
        self._init_arguments()
        plugin_name = kwargs.get('name') or self.name
        register(plugin_name, self.fct.__module__)

    @property
    def name(self):
        return self.fct.__name__.replace('_','-')

    @property
    def help(self):
        if getattr(self.fct, '__doc__', None):
            # just the first line of the doc string
            return self.fct.__doc__.splitlines()[0]

    @property
    def description(self):
        if getattr(self.fct, '__doc__', None):
            return textwrap.dedent(self.fct.__doc__)

    @property
    def add_argument(self):
        return self.parser.add_argument

    def __call__(self, *args, **kwargs):
        return self.fct(*args, **kwargs)
    
    def _make_parser(self, **kwargs):
        """Create and register a subparser for this command."""

        kwargs.setdefault('help', self.help)
        kwargs.setdefault('formatter_class',argparse.RawDescriptionHelpFormatter)
        kwargs.setdefault('description', self.description)
        kwargs.setdefault('name', self.name)
        names = (kwargs.get('name') or self.name).split('.')
        
        def _get_subparser(a):
            if a._subparsers:
                for action in a._subparsers._actions:
                    if isinstance(action, argparse._SubParsersAction):
                        return action
                raise RuntimeError('could not find adequate subparser')
            return a.add_subparsers(dest='command',
                                    title='commands',
                                    metavar='COMMAND')
        def _get_parser(node, idx, names):
            name = names[idx]
            if name in node.choices:
                return node.choices[name]
            args = {
                'name' : name,
                'help' : 'a group of sub-commands',
                }
            return node.add_parser(**args)
        
        parser = ACMD_PARSER
        node   = _get_subparser(parser)

        for i,n in enumerate(names[:-1]):
            node = _get_subparser(parser)
            parser = _get_parser(node, i, names)
                
        node = _get_subparser(parser)
        kwargs['name'] = names[-1]
        parser = node.add_parser(**kwargs)
        return parser

    def _init_arguments(self):
        if hasattr(self.fct, '_acmdlib_arguments'):
            while self.fct._acmdlib_arguments:
                args, kwargs = self.fct._acmdlib_arguments.pop()
                self.add_argument(*args, **kwargs)
        
    pass # Command

class Plugins(object):
    """A simple plugin registration.
    """
    _plugins = {}

    @classmethod
    def register(cls, name, value):
        """Register plugin
        """
        cls._plugins[name] = value

    @classmethod
    def load(cls, name):
        """Load given plugin and return it
        """
        try:
            return importlib.import_module(cls._plugins[name])
        except Exception as err:
            print("** could not load command [%s]:\n%s" % (name, err))

    @classmethod
    def loadAll(cls):
        """Load all plugins
        """
        for k in cls._plugins.keys():
            cls.load(k)

### functions -----------------------------------------------------------------

def command(*args, **kwargs):
    """Decorator to declare that a function is a command.
    """
    def deco(fct):
        return Command(fct, **kwargs)
    if args:
        return deco(*args)
    return deco

def argument(*args, **kwargs):
    """Decorator to add an argument to a command.
    """
    def deco(fct):
        if isinstance(fct, Command):
            cmd = fct
            cmd.add_argument(*args, **kwargs)
        else:
            if not hasattr(fct, '_acmdlib_arguments'):
                fct._acmdlib_arguments = []
            fct._acmdlib_arguments.append((args, kwargs))
        #print "===",args,kwargs,type(fct),fct
        return fct
    return deco
    
def register(name, value):
    """Registers a plugin, given a name and value.

    ex: register('check-file', 'PyUtils.CheckFileLib:fct')
    """
    return Plugins.register(name, value)