diff --git a/cement/core/foundation.py b/cement/core/foundation.py index dcace17d..ee05bc56 100644 --- a/cement/core/foundation.py +++ b/cement/core/foundation.py @@ -13,6 +13,7 @@ from ..core.handler import HandlerManager from ..core.hook import HookManager from ..utils.misc import is_true, minimal_logger from ..utils import fs, misc +from ..ext.ext_argparse import ArgparseController as Controller # The `imp` module is deprecated in favor of `importlib` in 3.4, but it # wasn't introduced until 3.1. Finally, reload is a builtin on Python < 3 @@ -442,17 +443,6 @@ class App(meta.MetaMixin): A handler class that implements the Cache interface. """ - base_controller = None - """ - This is the base application controller. If a controller is set, - runtime operations are passed to the controller for command - dispatch and argument parsing when ``App.run()`` is called. - - Note that Cement will automatically set the ``base_controller`` to a - registered controller whose label is ``base`` (only if - ``base_controller`` is not currently set). - """ - extensions = [] """List of additional framework extensions to load.""" @@ -920,7 +910,7 @@ class App(meta.MetaMixin): if self.controller: return_val = self.controller._dispatch() else: - self._parse_args() + self._parse_args() # pragma: nocover LOG.debug('running post_run hook') for res in self.hook.run('post_run', self): @@ -1200,20 +1190,19 @@ class App(meta.MetaMixin): self.catch_signal(signum) def _resolve_handler(self, handler_type, handler_def, raise_error=True): - meta_defaults = {} - if type(handler_def) == str: - _meta_label = "%s.%s" % (handler_type, handler_def) - meta_defaults = self._meta.meta_defaults.get(_meta_label, {}) - elif hasattr(handler_def, 'Meta'): - _meta_label = "%s.%s" % (handler_type, handler_def.Meta.label) - meta_defaults = self._meta.meta_defaults.get(_meta_label, {}) + # meta_defaults = {} + # if type(handler_def) == str: + # _meta_label = "%s.%s" % (handler_type, handler_def) + # meta_defaults = self._meta.meta_defaults.get(_meta_label, {}) + # elif hasattr(handler_def, 'Meta'): + # _meta_label = "%s.%s" % (handler_type, handler_def.Meta.label) + # meta_defaults = self._meta.meta_defaults.get(_meta_label, {}) - han = self.handler.resolve(handler_type, handler_def, + han = self.handler.resolve(handler_type, + handler_def, raise_error=raise_error, - meta_defaults=meta_defaults) - if han is not None: - han._setup(self) - return han + setup=True) + return han def _setup_extension_handler(self): LOG.debug("setting up %s.extension handler" % self._meta.label) @@ -1527,21 +1516,22 @@ class App(meta.MetaMixin): def _setup_controllers(self): LOG.debug("setting up application controllers") - if self._meta.base_controller is not None: - cntr = self._resolve_handler('controller', - self._meta.base_controller) - self.controller = cntr - self._meta.base_controller = self.controller - elif self._meta.base_controller is None: - if self.handler.registered('controller', 'base'): - self.controller = self._resolve_handler('controller', 'base') - self._meta.base_controller = self.controller + if self.handler.registered('controller', 'base'): + self.controller = self._resolve_handler('controller', 'base') - # This is necessary for some backend usage - if self._meta.base_controller is not None: - if self._meta.base_controller._meta.label != 'base': - raise exc.FrameworkError("Base controllers must have " + - "a label of 'base'.") + else: + class DefaultBaseController(Controller): + class Meta: + label = 'base' + + def _default(self): + # don't enforce anything cause developer might not be + # using controllers... if they are, they should define + # a base controller. + pass + + self.handler.register(DefaultBaseController) + self.controller = self._resolve_handler('controller', 'base') def validate_config(self): """ @@ -1702,6 +1692,5 @@ class TestApp(App): label = "app-%s" % misc.rando()[:12] config_files = [] argv = [] - base_controller = None arguments = [] exit_on_close = False diff --git a/cement/core/handler.py b/cement/core/handler.py index 80d4fad1..a2349cc1 100644 --- a/cement/core/handler.py +++ b/cement/core/handler.py @@ -335,7 +335,7 @@ class HandlerManager(object): to resolve the handler. meta_defaults (dict): Optional meta-data dictionary used as defaults to pass when instantiating uninstantiated handlers. - See ``App.Meta.meta_defaults``. + Use ``App.Meta.meta_defaults`` by default. setup (bool): Whether or not to call ``.setup()`` before return. Default: ``False`` @@ -357,7 +357,18 @@ class HandlerManager(object): """ raise_error = kwargs.get('raise_error', True) - meta_defaults = kwargs.get('meta_defaults', {}) + meta_defaults = kwargs.get('meta_defaults', None) + if meta_defaults is None: + meta_defaults = {} + if type(handler_def) == str: + _meta_label = "%s.%s" % (interface, handler_def) + meta_defaults = self.app._meta.meta_defaults.get(_meta_label, + {}) + elif hasattr(handler_def, 'Meta'): + _meta_label = "%s.%s" % (interface, handler_def.Meta.label) + meta_defaults = self.app._meta.meta_defaults.get(_meta_label, + {}) + setup = kwargs.get('setup', False) han = None diff --git a/cement/ext/ext_argparse.py b/cement/ext/ext_argparse.py index fff3f3aa..02a5b50b 100644 --- a/cement/ext/ext_argparse.py +++ b/cement/ext/ext_argparse.py @@ -339,8 +339,7 @@ class ArgparseController(ControllerHandler): if contr == self.__class__: continue - contr = contr() - contr._setup(self.app) + contr = self.app.handler.resolve('controller', contr, setup=True) unresolved_controllers.append(contr) # treat self/base separately diff --git a/cement/ext/ext_generate.py b/cement/ext/ext_generate.py index 03bd8265..c6b37f83 100644 --- a/cement/ext/ext_generate.py +++ b/cement/ext/ext_generate.py @@ -7,7 +7,7 @@ import os import inspect import yaml import shutil -from .. import Controller, minimal_logger, shell, FrameworkError +from .. import Controller, minimal_logger, shell from ..utils.version import VERSION, get_version LOG = minimal_logger(__name__) @@ -133,14 +133,6 @@ def setup_template_items(app): template_dirs = [] template_items = [] - # nothing will work without a base controller - try: - assert app._meta.base_controller is not None, \ - "The ext.generate extension requires an application base " + \ - "controller, but none is defined!" - except AssertionError as e: - raise FrameworkError(e.args[0]) - # look in app template dirs for path in app._meta.template_dirs: subpath = os.path.join(path, 'generate') diff --git a/cement/ext/ext_scrub.py b/cement/ext/ext_scrub.py index 81a2d1c8..d83afcc3 100644 --- a/cement/ext/ext_scrub.py +++ b/cement/ext/ext_scrub.py @@ -3,6 +3,7 @@ Cement scrub extension module. """ import re +from .. import Controller from ..utils.misc import minimal_logger LOG = minimal_logger(__name__) @@ -27,22 +28,39 @@ def extend_scrub(app): app.extend('scrub', scrub) - if hasattr(app._meta, 'scrub_argument'): - arg = app._meta.scrub_argument - else: - arg = ['--scrub'] - if hasattr(app._meta, 'scrub_argument_help'): - arg_help = app._meta.scrub_argument_help - else: - arg_help = 'obfuscate sensitive data from output' +class ScrubController(Controller): + """ + Add embedded options to the base controller to support scrubbing output. + """ - app.args.add_argument(*arg, - help=arg_help, - action='store_true', - dest='scrub') + class Meta: + #: Controller label + label = 'scrub' + + #: Parent controller to stack ontop of + stacked_on = 'base' + + #: Stacking method + stacked_type = 'embedded' + + #: Command line argument options + argument_options = ['--scrub'] + + #: Command line argument options help + argument_help = 'obfuscate sensitive data from rendered output' + + def _pre_argument_parsing(self): + if self._meta.argument_options is not None: + assert isinstance(self._meta.argument_options, list), \ + "ScrubController.Meta.argument_options must be a list" + self.app.args.add_argument(*self._meta.argument_options, + help=self._meta.argument_help, + action='store_true', + dest='scrub') def load(app): + app.handler.register(ScrubController) app.hook.register('post_render', scrub_output) app.hook.register('pre_argument_parsing', extend_scrub) diff --git a/tests/core/test_foundation.py b/tests/core/test_foundation.py index d8f09383..8b2b7010 100644 --- a/tests/core/test_foundation.py +++ b/tests/core/test_foundation.py @@ -325,17 +325,6 @@ def test_config_files_is_none(): assert f in app._meta.config_files -def test_base_controller_label(): - class BogusBaseController(Controller): - class Meta: - label = 'bad_base_controller_label' - - msg = "must have a label of 'base'" - with pytest.raises(FrameworkError, match=msg): - with TestApp(base_controller=BogusBaseController): - pass - - def test_pargs(): with TestApp(argv=['--debug']) as app: app.run() @@ -483,7 +472,7 @@ def test_run_forever(): def handler(signum, frame): raise AssertionError('It ran forever!') - app = TestApp(base_controller=MyController, argv=['run-it']) + app = TestApp(handlers=[MyController], argv=['run-it']) # set the signal handler and a 5-second alarm signal.signal(signal.SIGALRM, handler) diff --git a/tests/ext/test_ext_argparse.py b/tests/ext/test_ext_argparse.py index 19dca33d..cad45eb6 100644 --- a/tests/ext/test_ext_argparse.py +++ b/tests/ext/test_ext_argparse.py @@ -575,7 +575,7 @@ def test_get_exposed_commands(): def cmd2_two(self): pass - with TestApp(base_controller=MyController) as app: + with TestApp(handlers=[MyController]) as app: app.run() assert 'cmd1' in app.controller._get_exposed_commands() assert 'cmd2-two' in app.controller._get_exposed_commands() @@ -591,5 +591,5 @@ def test_coverage(): def hidden(self): pass - with TestApp(base_controller=MyController) as app: + with TestApp(handlers=[MyController]) as app: app.run() diff --git a/tests/ext/test_ext_generate.py b/tests/ext/test_ext_generate.py index 51465564..26e4a5f0 100644 --- a/tests/ext/test_ext_generate.py +++ b/tests/ext/test_ext_generate.py @@ -1,7 +1,7 @@ import os import re from unittest.mock import patch -from cement import TestApp, Controller, FrameworkError +from cement import TestApp, Controller from cement.utils import shell from cement.utils.test import raises @@ -54,14 +54,6 @@ def test_generate(tmp): app.run() -def test_missing_base_controller(tmp): - argv = ['generate', 'test1', tmp.dir, '--defaults'] - with TestApp(argv=argv, extensions=['jinja2', 'generate']) as app: - msg = 'ext.generate extension requires an application base controller' - with raises(FrameworkError, match=msg): - app.run() - - def test_prompt(tmp): argv = ['generate', 'test1', tmp.dir] diff --git a/tests/ext/test_ext_scrub.py b/tests/ext/test_ext_scrub.py index 258304b5..c8e3f5fb 100644 --- a/tests/ext/test_ext_scrub.py +++ b/tests/ext/test_ext_scrub.py @@ -1,4 +1,5 @@ +from cement import init_defaults from cement.utils.test import TestApp @@ -22,11 +23,13 @@ def test_scrub(): def test_argument(): + META = init_defaults('controller.scrub') + META['controller.scrub']['argument_options'] = ['--not-scrub'] + META['controller.scrub']['argument_help'] = 'not scrub' class MyScrubApp(ScrubApp): class Meta: - scrub_argument = ['--not-scrub'] - scrub_argument_help = 'not scrub' + meta_defaults = META with MyScrubApp(argv=['--not-scrub']) as app: app.run()