Drop CementBaseController

This commit is contained in:
BJ Dierkes 2017-02-04 22:16:54 -06:00
parent 1c1385dc86
commit 47e335a827
7 changed files with 17 additions and 794 deletions

View File

@ -69,9 +69,10 @@ class IController(interface.Interface):
.. code-block:: python
from cement.core import controller
from cement.core.handler import Handler
from cement.core.controller import IController
class MyBaseController(controller.CementBaseController):
class MyController(handler):
class Meta:
interface = controller.IController
...
@ -120,411 +121,3 @@ class IController(interface.Interface):
"""
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 or func.__doc__
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 self.app.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_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)

View File

@ -11,9 +11,7 @@ Requirements
This extension currently only works when using
:class:`cement.ext.ext_argparse.ArgparseArgumentHandler` (default) and
:class:`cement.ext.ext_argparse.ArgparseController` (new in Cement 2.8). It
will not work with :class:`cement.core.controller.CementBaseController`.
:class:`cement.ext.ext_argparse.ArgparseArgumentHandler` (default).
Configuration

View File

@ -334,10 +334,6 @@ class ArgparseController(CementBaseHandler):
``argparse``. If using an alternative argument handler you will need to
write your own controller base class or modify this one.
NOTE: This is a re-implementation of
:class:`cement.core.controller.CementBaseController`.
In the future, this class will eventually replace it as the default.
Usage:
.. code-block:: python

View File

@ -112,15 +112,14 @@ rather than the entire parent application. For example:
.. code-block:: python
from cement import App
from cement.core.controller import CementBaseController, expose
from cement import App, Controller, ex
class MyBaseController(CementBaseController):
class MyBaseController(Controller):
class Meta:
label = 'base'
@expose(help="run the daemon command.")
@ex(help="run the daemon command.")
def run_forever(self):
from time import sleep
self.app.daemonize()

View File

@ -97,17 +97,16 @@ perform an arbitrary action any time configuration changes are detected.
from time import sleep
from cement.core.exc import CaughtSignal
from cement import App
from cement.core.controller import CementBaseController, expose
from cement import App, Controller, ex
def print_foo(app):
print "Foo => %s" % app.config.get('myapp', 'foo')
class Base(CementBaseController):
class Base(Controller):
class Meta:
label = 'base'
@expose(hide=True)
@ex(hide=True)
def default(self):
print('Inside Base.default()')

View File

@ -8,366 +8,5 @@ from cement.utils.misc import rando, init_defaults
APP = "app-%s" % rando()[:12]
class TestController(controller.CementBaseController):
class Meta:
label = 'base'
arguments = [
(['-f', '--foo'], dict(help='foo option'))
]
usage = 'My Custom Usage TXT'
epilog = "This is the epilog"
@controller.expose(hide=True)
def default(self):
pass
@controller.expose()
def some_command(self):
pass
class TestWithPositionalController(controller.CementBaseController):
class Meta:
label = 'base'
arguments = [
(['foo'], dict(help='foo option', nargs='?'))
]
@controller.expose(hide=True)
def default(self):
self.app.render(dict(foo=self.app.pargs.foo))
class Embedded(controller.CementBaseController):
class Meta:
label = 'embedded_controller'
stacked_on = 'base'
stacked_type = 'embedded'
arguments = [(['-t'], dict())]
@controller.expose(aliases=['emcmd1'], help='This is my help txt')
def embedded_cmd1(self):
pass
class Nested(controller.CementBaseController):
class Meta:
label = 'nested_controller'
stacked_on = 'base'
stacked_type = 'nested'
arguments = [(['-t'], dict())]
@controller.expose()
def nested_cmd1(self):
pass
class AliasesOnly(controller.CementBaseController):
class Meta:
label = 'aliases_only_controller'
stacked_on = 'base'
stacked_type = 'nested'
aliases = ['this_is_ao_controller']
aliases_only = True
@controller.expose(aliases=['ao_cmd1'], aliases_only=True)
def aliases_only_cmd1(self):
pass
@controller.expose(aliases=['ao_cmd2', 'ao2'], aliases_only=True)
def aliases_only_cmd2(self):
pass
class DuplicateCommand(controller.CementBaseController):
class Meta:
label = 'duplicate_command'
stacked_on = 'base'
stacked_type = 'embedded'
@controller.expose()
def default(self):
pass
class DuplicateAlias(controller.CementBaseController):
class Meta:
label = 'duplicate_command'
stacked_on = 'base'
stacked_type = 'embedded'
@controller.expose(aliases=['default'])
def cmd(self):
pass
class Bad(controller.CementBaseController):
class Meta:
label = 'bad_controller'
arguments = []
class BadStackedType(controller.CementBaseController):
class Meta:
label = 'bad_stacked_type'
stacked_on = 'base'
stacked_type = 'bogus'
arguments = []
class ArgumentConflict(controller.CementBaseController):
class Meta:
label = 'embedded'
stacked_on = 'base'
stacked_type = 'embedded'
arguments = [(['-f', '--foo'], dict())]
class Unstacked(controller.CementBaseController):
class Meta:
label = 'unstacked'
stacked_on = None
arguments = [
(['--foo6'], dict(dest='foo6')),
]
class BadStackType(controller.CementBaseController):
class Meta:
label = 'bad_stack_type'
stacked_on = 'base'
stacked_type = 'bogus_stacked_type'
arguments = [
(['--foo6'], dict(dest='foo6')),
]
class ControllerTestCase(test.CementCoreTestCase):
def test_default(self):
app = self.make_app(base_controller=TestController)
app.setup()
app.run()
def test_epilog(self):
app = self.make_app(base_controller=TestController)
app.setup()
app.run()
self.eq(app.args.epilog, 'This is the epilog')
def test_txt_defined_base_controller(self):
self.app.handler.register(TestController)
self.app.setup()
@test.raises(exc.InterfaceError)
def test_invalid_arguments_1(self):
Bad.Meta.arguments = ['this is invalid']
self.app.handler.register(Bad)
@test.raises(exc.InterfaceError)
def test_invalid_arguments_2(self):
Bad.Meta.arguments = [('this is also invalid', dict())]
self.app.handler.register(Bad)
@test.raises(exc.InterfaceError)
def test_invalid_arguments_3(self):
Bad.Meta.arguments = [(['-f'], 'and this is invalid')]
self.app.handler.register(Bad)
@test.raises(exc.InterfaceError)
def test_invalid_arguments_4(self):
Bad.Meta.arguments = 'totally jacked'
self.app.handler.register(Bad)
def test_embedded_controller(self):
app = self.make_app(argv=['embedded-cmd1'])
app.handler.register(TestController)
app.handler.register(Embedded)
app.setup()
app.run()
check = 'embedded-cmd1' in app.controller._visible_commands
self.ok(check)
# also check for the alias here
check = 'emcmd1' in app.controller._dispatch_map
self.ok(check)
def test_nested_controller(self):
app = self.make_app(argv=['nested-controller'])
app.handler.register(TestController)
app.handler.register(Nested)
app.setup()
app.run()
check = 'nested-controller' in app.controller._visible_commands
self.ok(check)
self.eq(app.controller._dispatch_command['func_name'], '_dispatch')
def test_aliases_only_controller(self):
app = self.make_app(argv=['aliases-only-controller'])
app.handler.register(TestController)
app.handler.register(AliasesOnly)
app.setup()
app.run()
@test.raises(exc.FrameworkError)
def test_bad_stacked_type(self):
app = self.make_app()
app.handler.register(TestController)
app.handler.register(BadStackedType)
app.setup()
app.run()
@test.raises(exc.FrameworkError)
def test_duplicate_command(self):
app = self.make_app()
app.handler.register(TestController)
app.handler.register(DuplicateCommand)
app.setup()
app.run()
@test.raises(exc.FrameworkError)
def test_duplicate_alias(self):
app = self.make_app()
app.handler.register(TestController)
app.handler.register(DuplicateAlias)
app.setup()
app.run()
def test_usage_txt(self):
app = self.make_app()
app.handler.register(TestController)
app.setup()
self.eq(app.controller._usage_text, 'My Custom Usage TXT')
@test.raises(exc.FrameworkError)
def test_argument_conflict(self):
try:
app = self.make_app(base_controller=TestController)
app.handler.register(ArgumentConflict)
app.setup()
app.run()
except NameError as e:
# This is a hack due to a Travis-CI Bug:
# https://github.com/travis-ci/travis-ci/issues/998
if e.args[0] == "global name 'ngettext' is not defined":
bug = "https://github.com/travis-ci/travis-ci/issues/998"
raise test.SkipTest("Travis-CI Bug: %s" % bug)
else:
raise
def test_default_command_with_positional(self):
app = self.make_app(base_controller=TestWithPositionalController,
argv=['mypositional'])
app.setup()
app.run()
self.eq(app.get_last_rendered()[0]['foo'], 'mypositional')
def test_load_extensions_from_config_list(self):
defaults = init_defaults(APP)
defaults[APP]['extensions'] = ['json', 'yaml']
app = self.make_app(
label=APP,
extensions=[],
config_defaults=defaults,
)
app.setup()
app.run()
res = 'cement.ext.ext_json' in app.ext._loaded_extensions
self.ok(res)
res = 'cement.ext.ext_yaml' in app.ext._loaded_extensions
self.ok(res)
def test_load_extensions_from_config_str(self):
defaults = init_defaults(APP)
defaults[APP]['extensions'] = 'json, yaml'
app = self.make_app(
label=APP,
extensions=[],
config_defaults=defaults,
)
app.setup()
app.run()
res = 'cement.ext.ext_json' in app.ext._loaded_extensions
self.ok(res)
res = 'cement.ext.ext_yaml' in app.ext._loaded_extensions
self.ok(res)
@test.raises(exc.InterfaceError)
def test_invalid_stacked_on(self):
self.reset_backend()
try:
self.app = self.make_app(APP,
handlers=[
TestController,
Unstacked,
],
)
with self.app as app:
app.run()
except exc.InterfaceError as e:
self.ok(re.match("(.*)is not stacked anywhere!(.*)", e.msg))
raise
@test.raises(exc.InterfaceError)
def test_invalid_stacked_type(self):
self.reset_backend()
try:
self.app = self.make_app(APP,
handlers=[
TestController,
BadStackType,
],
)
with self.app as app:
app.run()
except exc.InterfaceError as e:
self.ok(re.match("(.*)has an unknown stacked type(.*)", e.msg))
raise
def test_usage_text(self):
self.reset_backend()
self.app = self.make_app(APP,
handlers=[
TestController,
],
)
with self.app as app:
self.app.controller._meta.usage = None
usage = app.controller._usage_text
self.ok(usage.startswith('%s (sub-commands ...)' %
self.app._meta.label))
def test_help_text(self):
self.reset_backend()
self.app = self.make_app(APP,
handlers=[
TestController,
AliasesOnly,
],
)
with self.app as app:
app.run()
app.controller._help_text
# self.ok(usage.startswith('%s (sub-commands ...)' % \
# self.app._meta.label))
pass

View File

@ -4,12 +4,11 @@ import os
import sys
import json
import signal
from cement import App
from cement import App, Controller, ex
from cement.core.foundation import cement_signal_handler
from cement.core import exc, extension
from cement.core.handler import CementBaseHandler
from cement.core.controller import CementBaseController, expose
from cement.core import output, hook, controller
from cement.core import output, hook
from cement.core.interface import Interface
from cement.utils import test
from cement.utils.misc import init_defaults, rando, minimal_logger
@ -59,7 +58,7 @@ class TestOutputHandler(output.CementOutputHandler):
return None
class BogusBaseController(controller.CementBaseController):
class BogusBaseController(Controller):
class Meta:
label = 'bad_base_controller_label'
@ -483,15 +482,15 @@ class FoundationTestCase(test.CementCoreTestCase):
@test.raises(AssertionError)
def test_run_forever(self):
class Controller(CementBaseController):
class MyController(Controller):
class Meta:
label = 'base'
@expose()
@ex()
def runit(self):
raise Exception("Fake some error")
app = self.make_app(base_controller=Controller, argv=['runit'])
app = self.make_app(base_controller=MyController, argv=['runit'])
def handler(signum, frame):
raise AssertionError('It ran forever!')