123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532 |
- """Cement core controller module."""
- import re
- import textwrap
- import argparse
- from ..core import exc, interface, handler
- from ..utils.misc import minimal_logger
- LOG = minimal_logger(__name__)
- def controller_validator(klass, obj):
- """
- Validates a handler implementation against the IController interface.
- """
- members = [
- '_setup',
- '_dispatch',
- ]
- meta = [
- 'label',
- 'interface',
- 'config_section',
- 'config_defaults',
- 'stacked_on',
- 'stacked_type',
- ]
- interface.validate(IController, obj, members, meta=meta)
- # also check _meta.arguments values
- errmsg = "Controller arguments must be a list of tuples. I.e. " + \
- "[ (['-f', '--foo'], dict(action='store')), ]"
- if obj._meta.arguments is not None:
- if type(obj._meta.arguments) is not list:
- raise exc.InterfaceError(errmsg)
- for item in obj._meta.arguments:
- if type(item) is not tuple:
- raise exc.InterfaceError(errmsg)
- if type(item[0]) is not list:
- raise exc.InterfaceError(errmsg)
- if type(item[1]) is not dict:
- raise exc.InterfaceError(errmsg)
- if not obj._meta.label == 'base' and obj._meta.stacked_on is None:
- errmsg = "Controller `%s` is not stacked anywhere!" % \
- obj.__class__.__name__
- raise exc.InterfaceError(errmsg)
- if not obj._meta.label == 'base' and \
- obj._meta.stacked_type not in ['nested', 'embedded']:
- raise exc.InterfaceError(
- "Controller '%s' " % obj._meta.label +
- "has an unknown stacked type of '%s'." %
- obj._meta.stacked_type
- )
- class IController(interface.Interface):
- """
- This class defines the Controller Handler Interface. Classes that
- implement this handler must provide the methods and attributes defined
- below.
- Implementations do *not* subclass from interfaces.
- Usage:
- .. code-block:: python
- from cement.core import controller
- class MyBaseController(controller.CementBaseController):
- class Meta:
- interface = controller.IController
- ...
- """
- # pylint: disable=W0232, C0111, R0903
- class IMeta:
- """Interface meta-data."""
- #: The string identifier of the interface.
- label = 'controller'
- #: The interface validator function.
- validator = controller_validator
- # Must be provided by the implementation
- Meta = interface.Attribute('Handler meta-data')
- def _setup(app_obj):
- """
- The _setup function is after application initialization and after it
- is determined that this controller was requested via command line
- arguments. Meaning, a controllers _setup() function is only called
- right before it's _dispatch() function is called to execute a command.
- Must 'setup' the handler object making it ready for the framework
- or the application to make further calls to it.
- :param app_obj: The application object.
- :returns: ``None``
- """
- def _dispatch(self):
- """
- Reads the application object's data to dispatch a command from this
- controller. For example, reading self.app.pargs to determine what
- command was passed, and then executing that command function.
- Note that Cement does *not* parse arguments when calling _dispatch()
- on a controller, as it expects the controller to handle parsing
- arguments (I.e. self.app.args.parse()).
- :returns: Returns the result of the executed controller function,
- or ``None`` if no controller function is called.
- """
- class expose(object):
- """
- Used to expose controller functions to be listed as commands, and to
- decorate the function with Meta data for the argument parser.
- :param help: Help text to display for that command.
- :type help: str
- :param hide: Whether the command should be visible.
- :type hide: boolean
- :param aliases: Aliases to this command.
- :param aliases_only: Whether to only display the aliases (not the label).
- This is useful for situations where you have obscure function names
- which you do not want displayed. Effecively, if there are aliases and
- `aliases_only` is True, then aliases[0] will appear as the actual
- command/function label.
- :type aliases: ``list``
- Usage:
- .. code-block:: python
- from cement.core.controller import CementBaseController, expose
- class MyAppBaseController(CementBaseController):
- class Meta:
- label = 'base'
- @expose(hide=True, aliases=['run'])
- def default(self):
- print("In MyAppBaseController.default()")
- @expose()
- def my_command(self):
- print("In MyAppBaseController.my_command()")
- """
- # pylint: disable=W0622
- def __init__(self, help='', hide=False, aliases=[], aliases_only=False):
- self.hide = hide
- self.help = help
- self.aliases = aliases
- self.aliases_only = aliases_only
- def __call__(self, func):
- metadict = {}
- metadict['label'] = re.sub('_', '-', func.__name__)
- metadict['func_name'] = func.__name__
- metadict['exposed'] = True
- metadict['hide'] = self.hide
- metadict['help'] = self.help
- metadict['aliases'] = self.aliases
- metadict['aliases_only'] = self.aliases_only
- metadict['controller'] = None # added by the controller
- func.__cement_meta__ = metadict
- return func
- # pylint: disable=R0921
- class CementBaseController(handler.CementBaseHandler):
- """
- This is an implementation of the
- `IControllerHandler <#cement.core.controller.IController>`_ interface, but
- as a base class that application controllers `should` subclass from.
- Registering it directly as a handler is useless.
- NOTE: This handler **requires** that the applications 'arg_handler' be
- argparse. If using an alternative argument handler you will need to
- write your own controller base class.
- NOTE: This the initial default implementation of CementBaseController. In
- the future it will be replaced by CementBaseController2, therefore using
- CementBaseController2 is recommended for new development.
- Usage:
- .. code-block:: python
- from cement.core.controller import CementBaseController
- class MyAppBaseController(CementBaseController):
- class Meta:
- label = 'base'
- description = 'MyApp is awesome'
- config_defaults = dict()
- arguments = []
- epilog = "This is the text at the bottom of --help."
- # ...
- class MyStackedController(CementBaseController):
- class Meta:
- label = 'second_controller'
- aliases = ['sec', 'secondary']
- stacked_on = 'base'
- stacked_type = 'embedded'
- # ...
- """
- class Meta:
- """
- Controller meta-data (can be passed as keyword arguments to the parent
- class).
- """
- interface = IController
- """The interface this class implements."""
- label = None
- """The string identifier for the controller."""
- aliases = []
- """
- A list of aliases for the controller. Will be treated like
- command/function aliases for non-stacked controllers. For example:
- ``myapp <controller_label> --help`` is the same as
- ``myapp <controller_alias> --help``.
- """
- aliases_only = False
- """
- When set to True, the controller label will not be displayed at
- command line, only the aliases will. Effectively, aliases[0] will
- appear as the label. This feature is useful for the situation Where
- you might want two controllers to have the same label when stacked
- on top of separate controllers. For example, 'myapp users list' and
- 'myapp servers list' where 'list' is a stacked controller, not a
- function.
- """
- description = None
- """The description shown at the top of '--help'. Default: None"""
- config_section = None
- """
- A config [section] to merge config_defaults into. Cement will default
- to controller.<label> if None is set.
- """
- config_defaults = {}
- """
- Configuration defaults (type: dict) that are merged into the
- applications config object for the config_section mentioned above.
- """
- arguments = []
- """
- Arguments to pass to the argument_handler. The format is a list
- of tuples whos items are a ( list, dict ). Meaning:
- ``[ ( ['-f', '--foo'], dict(dest='foo', help='foo option') ), ]``
- This is equivelant to manually adding each argument to the argument
- parser as in the following example:
- ``parser.add_argument(['-f', '--foo'], help='foo option', dest='foo')``
- """
- stacked_on = 'base'
- """
- A label of another controller to 'stack' commands/arguments on top of.
- """
- stacked_type = 'embedded'
- """
- Whether to `embed` commands and arguments within the parent controller
- or to simply `nest` the controller under the parent controller (making
- it a sub-sub-command). Must be one of `['embedded', 'nested']` only
- if `stacked_on` is not `None`.
- """
- hide = False
- """Whether or not to hide the controller entirely."""
- epilog = None
- """
- The text that is displayed at the bottom when '--help' is passed.
- """
- usage = None
- """
- The text that is displayed at the top when '--help' is passed.
- Although the default is `None`, Cement will set this to a generic
- usage based on the `prog`, `controller` name, etc if nothing else is
- passed.
- """
- argument_formatter = argparse.RawDescriptionHelpFormatter
- """
- The argument formatter class to use to display --help output.
- """
- default_func = 'default'
- """
- Function to call if no sub-command is passed. Note that this can
- **not** start with an ``_`` due to backward compatibility restraints
- in how Cement discovers and maps commands.
- """
- def __init__(self, *args, **kw):
- super(CementBaseController, self).__init__(*args, **kw)
- self.app = None
- self._commands = {} # used to store collected commands
- self._visible_commands = [] # used to sort visible command labels
- self._arguments = [] # used to store collected arguments
- self._dispatch_map = {} # used to map commands/aliases to controller
- self._dispatch_command = None # set during _parse_args()
- def _setup(self, app_obj):
- """
- See `IController._setup() <#cement.core.cache.IController._setup>`_.
- """
- super(CementBaseController, self)._setup(app_obj)
- if getattr(self._meta, 'description', None) is None:
- self._meta.description = "%s Controller" % \
- self._meta.label.capitalize()
- self.app = app_obj
- def _collect(self):
- LOG.debug("collecting arguments/commands for %s" % self)
- arguments = []
- commands = []
- # process my arguments and commands first
- arguments = list(self._meta.arguments)
- for member in dir(self.__class__):
- if member.startswith('_'):
- continue
- try:
- func = getattr(self.__class__, member).__cement_meta__
- except AttributeError:
- continue
- else:
- func['controller'] = self
- commands.append(func)
- # process stacked controllers second for commands and args
- for contr in handler.list('controller'):
- # don't include self here
- if contr == self.__class__:
- continue
- contr = contr()
- contr._setup(self.app)
- if contr._meta.stacked_on == self._meta.label:
- if contr._meta.stacked_type == 'embedded':
- contr_arguments, contr_commands = contr._collect()
- for arg in contr_arguments:
- arguments.append(arg)
- for func in contr_commands:
- commands.append(func)
- elif contr._meta.stacked_type == 'nested':
- metadict = {}
- metadict['label'] = re.sub('_', '-', contr._meta.label)
- metadict['func_name'] = '_dispatch'
- metadict['exposed'] = True
- metadict['hide'] = contr._meta.hide
- metadict['help'] = contr._meta.description
- metadict['aliases'] = contr._meta.aliases
- metadict['aliases_only'] = contr._meta.aliases_only
- metadict['controller'] = contr
- commands.append(metadict)
- return (arguments, commands)
- def _process_arguments(self):
- for _arg, _kw in self._arguments:
- try:
- self.app.args.add_argument(*_arg, **_kw)
- except argparse.ArgumentError as e:
- raise exc.FrameworkError(e.__str__())
- def _process_commands(self):
- self._dispatch_map = {}
- self._visible_commands = []
- for cmd in self._commands:
- # process command labels
- if cmd['label'] in self._dispatch_map.keys():
- raise exc.FrameworkError(
- "Duplicate command named '%s' " % cmd['label'] +
- "found in controller '%s'" % cmd['controller']
- )
- self._dispatch_map[cmd['label']] = cmd
- if not cmd['hide']:
- self._visible_commands.append(cmd['label'])
- # process command aliases
- for alias in cmd['aliases']:
- if alias in self._dispatch_map.keys():
- raise exc.FrameworkError(
- "The alias '%s' of the " % alias +
- "'%s' controller collides " % cmd['controller'] +
- "with a command or alias of the same name."
- )
- self._dispatch_map[alias] = cmd
- self._visible_commands.sort()
- def _get_dispatch_command(self):
- default_func = self._meta.default_func
- default_func_key = re.sub('_', '-', self._meta.default_func)
- if (len(self.app.argv) <= 0) or (self.app.argv[0].startswith('-')):
- # if no command is passed, then use default
- if default_func_key in self._dispatch_map.keys():
- self._dispatch_command = self._dispatch_map[default_func_key]
- elif self.app.argv[0] in self._dispatch_map.keys():
- self._dispatch_command = self._dispatch_map[self.app.argv[0]]
- self.app.argv.pop(0)
- else:
- # check for default again (will get here if command line has
- # positional arguments that don't start with a -)
- if default_func_key in self._dispatch_map.keys():
- self._dispatch_command = self._dispatch_map[default_func_key]
- def _parse_args(self):
- self.app.args.description = self._help_text
- self.app.args.usage = self._usage_text
- self.app.args.formatter_class = self._meta.argument_formatter
- self.app._parse_args()
- def _dispatch(self):
- """
- Takes the remaining arguments from self.app.argv and parses for a
- command to dispatch, and if so... dispatches it.
- """
- if hasattr(self._meta, 'epilog'):
- if self._meta.epilog is not None:
- self.app.args.epilog = self._meta.epilog
- self._arguments, self._commands = self._collect()
- self._process_commands()
- self._get_dispatch_command()
- if self._dispatch_command:
- if self._dispatch_command['func_name'] == '_dispatch':
- func = getattr(self._dispatch_command['controller'],
- '_dispatch')
- return func()
- else:
- self._process_arguments()
- self._parse_args()
- func = getattr(self._dispatch_command['controller'],
- self._dispatch_command['func_name'])
- return func()
- else:
- self._process_arguments()
- self._parse_args()
- @property
- def _usage_text(self):
- """Returns the usage text displayed when ``--help`` is passed."""
- if self._meta.usage is not None:
- return self._meta.usage
- txt = "%s (sub-commands ...) [options ...] {arguments ...}" % \
- self.app.args.prog
- return txt
- @property
- def _help_text(self):
- """Returns the help text displayed when '--help' is passed."""
- cmd_txt = ''
- for label in self._visible_commands:
- cmd = self._dispatch_map[label]
- if len(cmd['aliases']) > 0 and cmd['aliases_only']:
- if len(cmd['aliases']) > 1:
- first = cmd['aliases'].pop(0)
- cmd_txt = cmd_txt + " %s (aliases: %s)\n" % \
- (first, ', '.join(cmd['aliases']))
- else:
- cmd_txt = cmd_txt + " %s\n" % cmd['aliases'][0]
- elif len(cmd['aliases']) > 0:
- cmd_txt = cmd_txt + " %s (aliases: %s)\n" % \
- (label, ', '.join(cmd['aliases']))
- else:
- cmd_txt = cmd_txt + " %s\n" % label
- if cmd['help']:
- cmd_txt = cmd_txt + " %s\n\n" % cmd['help']
- else:
- cmd_txt = cmd_txt + "\n"
- if len(cmd_txt) > 0:
- txt = '''%s
- commands:
- %s
- ''' % (self._meta.description, cmd_txt)
- else:
- txt = self._meta.description
- return textwrap.dedent(txt)
|