diff --git a/ChangeLog b/ChangeLog index 0b9f1ad7..adfac752 100755 --- a/ChangeLog +++ b/ChangeLog @@ -35,6 +35,8 @@ Features: * :issue:`119` - Added cement.utils.shell.Prompt to quickly gather user input in several different ways. + * :issue:`229` - Ability to override handlers via command line options. + Also related: :issue:`225`. * :issue:`248` - Added documentation for BASH Auto-Completion example. * :issue:`249` - Allow utils.shell.exec_cmd* to accept additional. parameters, passing all `args` and `kwargs` down to subprocess.Popen. @@ -49,12 +51,15 @@ Features: * :issue:`269` - Allow app.close() to accept an exit code, and exit with that code. * :issue:`270` - Add support for multiple template_dirs + * :issue:`274` - Add cement.core.interface.list function Incompatible: * :issue:`227` - Standardize handler config section naming conventions. All handler config sections are now labeled after ``interface.handler`` (i.e. ``output.json``, or ``mail.sendgrid``, etc). + * :issue:`229` - Handler override options deprecate the use of ``--json``, + and ``--yaml`` output handler options. * :issue:`260` - Extensions/Plugins/Bootstrap modules must accept `app` argument in `load()` function. See Upgrading doc for more information. diff --git a/cement/core/arg.py b/cement/core/arg.py index 99065370..5412703d 100644 --- a/cement/core/arg.py +++ b/cement/core/arg.py @@ -82,6 +82,8 @@ class IArgument(interface.Interface): :keyword help: The help text for --help output (for that argument). :keyword action: Must support: ['store', 'store_true', 'store_false', 'store_const'] + :keyword choices: A list of valid values that can be passed to an + option whose action is ``store``. :keyword const: The value stored if action == 'store_const'. :keyword default: The default value. :returns: None diff --git a/cement/core/foundation.py b/cement/core/foundation.py index 2442ef4a..03419178 100644 --- a/cement/core/foundation.py +++ b/cement/core/foundation.py @@ -5,7 +5,8 @@ import os import sys import signal -from ..core import backend, exc, handler, hook, log, config, plugin + +from ..core import backend, exc, handler, hook, log, config, plugin, interface from ..core import output, extension, arg, controller, meta, cache from ..ext import ext_configparser, ext_argparse, ext_logging from ..ext import ext_nulloutput, ext_plugin @@ -14,71 +15,90 @@ from ..utils import fs if sys.version_info[0] >= 3: from imp import reload # pragma: nocover + from io import StringIO # pragma: nocover +else: + from StringIO import StringIO # pragma: nocover LOG = minimal_logger(__name__) class NullOut(object): - def write(self, s): pass def flush(self): pass -def add_output_handler_override_option(app): + +def add_handler_override_options(app): """ - This is a ``post_setup`` hook that adds the ``--json`` argument to the - argument object. + This is a ``post_setup`` hook that adds the handler override options to + the argument parser :param app: The application object. """ - if len(handler.list('output')) > 1: - output_handlers = [] - for h in handler.list('output'): - output_handlers.append(h()) + if app._meta.handler_override_options is None: + return - output_options = [x._meta.label - for x in output_handlers - if x._meta.display_override_option is True] + for i in app._meta.handler_override_options: + if i not in interface.list(): + LOG.debug("interface '%s'" % i + + " is not defined, can not override handlers") + continue - # don't display the option if not output handlers have - # display_override_option enabled - if len(output_options) > 0: + if len(handler.list(i)) > 1: + handlers = [] + for h in handler.list(i): + handlers.append(h()) - help_txt = "%s [%s]" % ( - app._meta.output_handler_override_help, - ', '.join(output_options) + choices = [x._meta.label + for x in handlers + if x._meta.overridable is True] + + # don't display the option if no handlers are overridable + if not len(choices) > 0: + LOG.debug("no handlers are overridable within the " + + "%s interface" % i) + continue + + # override things that we need to control + argument_kw = app._meta.handler_override_options[i][1] + argument_kw['dest'] = '%s_handler_override' % i + argument_kw['action'] = 'store' + argument_kw['choices'] = choices + + app.args.add_argument( + *app._meta.handler_override_options[i][0], + **app._meta.handler_override_options[i][1] ) - if app._meta.output_handler_override is not None: - app.args.add_argument( - *app._meta.output_handler_override, - help=help_txt, - dest='output_handler_handler', - action='store', - metavar='STR' - ) -def output_handler_override(app): +def handler_override(app): """ - This is a ``post_argument_parsing`` hook that overrides the configured - output handler if ``CementApp.Meta.output_handler_override`` is enabled - and is passed at the command line. + This is a ``post_argument_parsing`` hook that overrides a configured + handler if defined in ``CementApp.Meta.handler_override_options`` and + the option is passed at command line with a valid handler label. :param app: The application object. """ - if app._meta.output_handler_override is None: + if app._meta.handler_override_options is None: return - elif not hasattr(app.pargs, 'output_handler_override'): - return + for i in app._meta.handler_override_options.keys(): + if not hasattr(app.pargs, '%s_handler_override' % i): + continue + elif getattr(app.pargs, '%s_handler_override' % i) is None: + continue + else: + # get the argument value from command line + argument = getattr(app.pargs, '%s_handler_override' % i) + setattr(app._meta, '%s_handler' % i, argument) + + # and then re-setup the handler + getattr(app, '_setup_%s_handler' % i)() - if app.pargs.output_handler_override is not None: - app._meta.output_handler = app.pargs.output_handler_override - app._setup_output_handler() def cement_signal_handler(signum, frame): """ @@ -317,6 +337,43 @@ class CementApp(meta.MetaMixin): affect whether ``arguments_override_config`` is ``True`` or ``False``. """ + core_handler_override_options = dict( + output=(['-o'], dict(help='output handler')), + ) + """ + Similar to ``CementApp.Meta.handler_override_options`` but these are + the core defaults required by Cement. This dictionary can be + overridden by ``CementApp.Meta.handler_override_options`` (when they + are merged together). + """ + + handler_override_options = {} + """ + Dictionary of handler override options that will be added to the + argument parser, and allow the end-user to override handlers. Useful + for interfaces that have multiple uses within the same application + (for example: Output Handler (json, yaml, etc) or maybe a Cloud + Provider Handler (rackspace, digitalocean, amazon, etc). + + This dictionary will merge with + ``CementApp.Meta.core_handler_override_options`` but this one has + precedence. + + Dictionary Format: + + .. code-block:: text + + = (option_arguments, help_text) + + + See ``CementApp.Meta.core_handler_override_options`` for an example + of what this should look like. + + Note, if set to ``None`` then no options will be defined, and the + ``CementApp.Meta.core_meta_override_options`` will be ignore (not + recommended as some extensions rely on this feature). + """ + config_section = None """ The base configuration section for the application. @@ -380,19 +437,6 @@ class CementApp(meta.MetaMixin): class, or an instantiated class object. """ - output_handler_override = ['-o', '--output'] - """ - Options added to ``argument_handler`` command line arguments allowing - the user to override the ``output_handler`` (i.e. ``-o json``, - ``-o yaml``, etc. - """ - - output_handler_override_help = "output handler" - """ - Help text that is displayed for the - ``output_handler_override_option``. - """ - cache_handler = None """ A handler class that implements the ICache interface. This can @@ -517,12 +561,6 @@ class CementApp(meta.MetaMixin): ``template_dirs``. """ - suppress_output = False - """ - Used internally to suppress all console output (for example, when - ``--quiet`` is passed at command line). - """ - def __init__(self, label=None, **kw): super(CementApp, self).__init__(**kw) @@ -691,7 +729,7 @@ class CementApp(meta.MetaMixin): "Invalid exit status code (must be integer)" sys.exit(code) - def render(self, data, template=None): + def render(self, data, template=None, out=sys.stdout): """ This is a simple wrapper around self.output.render() which simply returns an empty string if no self.output handler is defined. @@ -699,6 +737,9 @@ class CementApp(meta.MetaMixin): :param data: The data dictionary to render. :param template: The template to render to. Default: None (some output handlers do not use templates). + :param out: A file like object (sys.stdout, or actual file). Set to + ``None`` is no output is desired (just render and return). + Default: sys.stdout """ for res in hook.run('pre_render', self, data): @@ -719,6 +760,13 @@ class CementApp(meta.MetaMixin): else: out_text = str(res) + if out is not None and not hasattr(out, 'write'): + raise TypeError("Argument 'out' must be a 'file' like object") + elif out is not None and out_text is None: + LOG.debug('render() called but output text is None') + elif out: + out.write(out_text) + self._last_rendered = (data, out_text) return out_text @@ -764,6 +812,22 @@ class CementApp(meta.MetaMixin): """A shortcut for self.args.add_argument.""" self.args.add_argument(*args, **kw) + def _suppress_output(self): + if self._meta.debug is True: + LOG.debug('not suppressing console output because of debug mode') + return + + LOG.debug('suppressing all console output') + backend.__saved_stdout__ = sys.stdout + backend.__saved_stderr__ = sys.stderr + sys.stdout = NullOut() + sys.stderr = NullOut() + + def _unsuppress_output(self): + LOG.debug('unsuppressing all console output') + sys.stdout = backend.__saved_stdout__ + sys.stderr = backend.__saved_stderr__ + def _lay_cement(self): """Initialize the framework.""" LOG.debug("laying cement for the '%s' application" % @@ -772,16 +836,7 @@ class CementApp(meta.MetaMixin): if '--debug' in self._meta.argv: self._meta.debug = True elif '--quiet' in self._meta.argv: - # the following are hacks to suppress console output - # for flag in ['--quiet', '--json', '--yaml']: - self._meta.suppress_output = True - - if self._meta.suppress_output: - LOG.debug('suppressing all console output per runtime config') - backend.__saved_stdout__ = sys.stdout - backend.__saved_stderr__ = sys.stderr - sys.stdout = NullOut() - sys.stderr = NullOut() + self._suppress_output() # start clean backend.__hooks__ = {} @@ -801,8 +856,8 @@ class CementApp(meta.MetaMixin): hook.define('post_render') # register some built-in framework hooks - hook.register('post_setup', add_output_handler_override_option) - hook.register('post_argument_parsing', output_handler_override) + hook.register('post_setup', add_handler_override_options, weight=-99) + hook.register('post_argument_parsing', handler_override, weight=-99) # define and register handlers handler.define(extension.IExtension) @@ -998,6 +1053,16 @@ class CementApp(meta.MetaMixin): action='store_true', help='suppress all output') + # merge handler override meta data + if self._meta.handler_override_options is not None: + # fucking long names... fuck. anyway, merge the core handler + # override options with developer defined options + core = self._meta.core_handler_override_options.copy() + dev = self._meta.handler_override_options.copy() + core.update(dev) + + self._meta.handler_override_options = core + def _setup_controllers(self): LOG.debug("setting up application controllers") diff --git a/cement/core/handler.py b/cement/core/handler.py index eb647404..c120a1a2 100644 --- a/cement/core/handler.py +++ b/cement/core/handler.py @@ -44,12 +44,12 @@ class CementBaseHandler(meta.MetaMixin): override any existing defaults under that section. """ - display_override_option = False + overridable = False """ - Whether or not to display this handlers label along with other - override options (if handler override options are enabled in - ``CementApp``). Generally used for things like - ``CementApp.Meta.output_handler_override`` + Whether or not handler can be overridden by + ``CementApp.Meta.handler_override_options``. Will be listed as an + available choice to override the specific handler (i.e. + ``CementApp.Meta.output_handler``, etc). """ def __init__(self, **kw): diff --git a/cement/core/interface.py b/cement/core/interface.py index 850afa16..77026b42 100644 --- a/cement/core/interface.py +++ b/cement/core/interface.py @@ -3,11 +3,22 @@ Cement core interface module. """ -from ..core import exc +from ..core import exc, backend DEFAULT_META = ['interface', 'label', 'config_defaults', 'config_section'] +def list(): + """ + Return a list of defined interfaces (handler types). + + :returns: List of defined interfaces + :rtype: list + + """ + return backend.__handlers__.keys() + + class Interface(object): """ diff --git a/cement/ext/ext_json.py b/cement/ext/ext_json.py index 72abca2a..3ae7ed9e 100644 --- a/cement/ext/ext_json.py +++ b/cement/ext/ext_json.py @@ -9,31 +9,48 @@ from ..ext.ext_configparser import ConfigParserConfigHandler LOG = minimal_logger(__name__) -# def add_json_option(app): -# """ -# This is a ``post_setup`` hook that adds the ``--json`` argument to the -# argument object. +def suppress_output_before_run(app): + """ + This is a ``post_argument_parsing`` hook that suppresses console output if + the ``JsonOutputHandler`` is triggered via command line. -# :param app: The application object. + :param app: The application object. -# """ -# app.args.add_argument('--json', dest='output_handler', -# action='store_const', -# help='toggle json output handler', -# const='json') + """ + if not hasattr(app.pargs, 'output_handler_override'): + return + elif app.pargs.output_handler_override == 'json': + app._suppress_output() -# def set_output_handler(app): -# """ -# This is a ``pre_run`` hook that overrides the configured output handler -# if ``--json`` is passed at the command line. +def unsuppress_output_before_render(app, data): + """ + This is a ``pre_render`` that unsuppresses console output if + the ``JsonOutputHandler`` is triggered via command line so that the JSON + is the only thing in the output. -# :param app: The application object. + :param app: The application object. -# """ -# if '--json' in app._meta.argv: -# app._meta.output_handler = 'json' -# app._setup_output_handler() + """ + if not hasattr(app.pargs, 'output_handler_override'): + return + elif app.pargs.output_handler_override == 'json': + app._unsuppress_output() + + +def suppress_output_after_render(app, out_text): + """ + This is a ``post_render`` hook that suppresses console output again after + rendering, only if the ``JsonOutputHandler`` is triggered via command + line. + + :param app: The application object. + + """ + if not hasattr(app.pargs, 'output_handler_override'): + return + elif app.pargs.output_handler_override == 'json': + app._suppress_output() class JsonOutputHandler(output.CementOutputHandler): @@ -44,10 +61,15 @@ class JsonOutputHandler(output.CementOutputHandler): library. Please see the developer documentation on :ref:`Output Handling `. - Note: The cement framework detects the '--json' option and suppresses - output (same as if passing --quiet). Therefore, if debugging or - troubleshooting issues you must pass the --debug option to see whats - going on. + Note: By default, Cement adds the ``-o`` command line option to allow the + end user to override the output handler. For example: passing ``-o json`` + will override the default output handler and set it to + ``JsonOutputHandler``. See ``CementApp.Meta.handler_override_options``. + + This extension forces Cement to suppress console output until + ``app.render`` is called (keeping the output pure JSON). If + troubleshooting issues, you will need to pass the ``--debug`` option in + order to unsuppress output and see what's happening. """ class Meta: @@ -60,7 +82,7 @@ class JsonOutputHandler(output.CementOutputHandler): label = 'json' """The string identifier of this handler.""" - display_override_option = True + overridable = True def __init__(self, *args, **kw): super(JsonOutputHandler, self).__init__(*args, **kw) @@ -78,8 +100,6 @@ class JsonOutputHandler(output.CementOutputHandler): """ LOG.debug("rendering output as Json via %s" % self.__module__) - sys.stdout = backend.__saved_stdout__ - sys.stderr = backend.__saved_stderr__ return json.dumps(data_dict) @@ -114,7 +134,8 @@ class JsonConfigHandler(ConfigParserConfigHandler): def load(app): - #hook.register('post_setup', add_json_option) - #hook.register('pre_run', set_output_handler) + hook.register('post_argument_parsing', suppress_output_before_run) + hook.register('pre_render', unsuppress_output_before_render) + hook.register('post_render', suppress_output_after_render) handler.register(JsonOutputHandler) handler.register(JsonConfigHandler) diff --git a/cement/ext/ext_yaml.py b/cement/ext/ext_yaml.py index e54a06cd..0269a6f3 100644 --- a/cement/ext/ext_yaml.py +++ b/cement/ext/ext_yaml.py @@ -10,6 +10,50 @@ from ..ext.ext_configparser import ConfigParserConfigHandler LOG = minimal_logger(__name__) +def suppress_output_before_run(app): + """ + This is a ``post_argument_parsing`` hook that suppresses console output if + the ``YamlOutputHandler`` is triggered via command line. + + :param app: The application object. + + """ + if not hasattr(app.pargs, 'output_handler_override'): + return + elif app.pargs.output_handler_override == 'yaml': + app._suppress_output() + + +def unsuppress_output_before_render(app, data): + """ + This is a ``pre_render`` that unsuppresses console output if + the ``YamlOutputHandler`` is triggered via command line so that the YAML + is the only thing in the output. + + :param app: The application object. + + """ + if not hasattr(app.pargs, 'output_handler_override'): + return + elif app.pargs.output_handler_override == 'yaml': + app._unsuppress_output() + + +def suppress_output_after_render(app, out_text): + """ + This is a ``post_render`` hook that suppresses console output again after + rendering, only if the ``YamlOutputHandler`` is triggered via command + line. + + :param app: The application object. + + """ + if not hasattr(app.pargs, 'output_handler_override'): + return + elif app.pargs.output_handler_override == 'yaml': + app._suppress_output() + + class YamlOutputHandler(output.CementOutputHandler): """ This class implements the :ref:`IOutput ` @@ -18,20 +62,21 @@ class YamlOutputHandler(output.CementOutputHandler): STDOUT. Please see the developer documentation on :ref:`Output Handling `. - **Note** The cement framework detects the '--yaml' option and suppresses - output (same as if passing --quiet). Therefore, if debugging or - troubleshooting issues you must pass the --debug option to see whats - going on. + Note: By default, Cement adds the ``-o`` command line option to allow the + end user to override the output handler. For example: passing ``-o yaml`` + will override the default output handler and set it to + ``YamlOutputHandler``. See ``CementApp.Meta.handler_override_options``. - **Note** This extension has an external dependency on `pyYAML`. You must - include `pyYAML` in your application's dependencies as Cement explicitly - does *not* include external dependencies for optional extensions. + This extension forces Cement to suppress console output until + ``app.render`` is called (keeping the output pure YAML). If + troubleshooting issues, you will need to pass the ``--debug`` option in + order to unsuppress output and see what's happening. """ class Meta: interface = output.IOutput label = 'yaml' - display_override_option = True + overridable = True def __init__(self, *args, **kw): super(YamlOutputHandler, self).__init__(*args, **kw) @@ -58,34 +103,6 @@ class YamlOutputHandler(output.CementOutputHandler): return yaml.dump(data_dict) -# def add_yaml_option(app): -# """ -# This is a ``post_setup`` hook that adds the ``--yaml`` argument to the -# command line. - -# :param app: The application object. - -# """ -# app.args.add_argument('--yaml', -# dest='output_handler', -# action='store_const', -# help='toggle yaml output handler', -# const='yaml') - - -# def set_output_handler(app): -# """ -# This is a ``pre_run`` hook that overrides the configured output handler -# if ``--yaml`` is passed at the command line. - -# :param app: The application object. - -# """ -# if '--yaml' in app._meta.argv: -# app._meta.output_handler = 'yaml' -# app._setup_output_handler() - - class YamlConfigHandler(ConfigParserConfigHandler): """ This class implements the :ref:`IConfig ` @@ -123,7 +140,8 @@ class YamlConfigHandler(ConfigParserConfigHandler): def load(app): + hook.register('post_argument_parsing', suppress_output_before_run) + hook.register('pre_render', unsuppress_output_before_render) + hook.register('post_render', suppress_output_after_render) handler.register(YamlOutputHandler) handler.register(YamlConfigHandler) - #hook.register('post_setup', add_yaml_option) - #hook.register('pre_run', set_output_handler) diff --git a/cement/utils/test.py b/cement/utils/test.py index 1495892a..f46dcba3 100644 --- a/cement/utils/test.py +++ b/cement/utils/test.py @@ -1,6 +1,7 @@ """Cement testing utilities.""" import unittest +from tempfile import mkstemp, mkdtemp from ..core import backend, foundation # shortcuts @@ -45,6 +46,8 @@ class CementTestCase(unittest.TestCase): """ self.app = self.make_app() + _, self.tmp_file = mkstemp() + self.tmp_dir = mkdtemp() def make_app(self, *args, **kw): """ diff --git a/doc/source/dev/interfaces_and_handlers.rst b/doc/source/dev/interfaces_and_handlers.rst index 7b8cbc5f..b1fad624 100644 --- a/doc/source/dev/interfaces_and_handlers.rst +++ b/doc/source/dev/interfaces_and_handlers.rst @@ -254,9 +254,9 @@ Multiple Registered Handlers All handlers and interfaces are unique. In most cases, where the framework is concerned, only one handler is used. For example, whatever is configured for the ``log_handler`` will be used and setup as ``app.log``. However, take -for example an Output handler. You might have a default ``output_handler`` of +for example an Output Handler. You might have a default ``output_handler`` of ``mustache``' (a text templating language) but may also want to override that -handler with the ``json`` output handler when '--json' is passed at command +handler with the ``json`` output handler when ``-o json`` is passed at command line. In order to allow this functionality, both the ``mustache`` and ``json`` output handlers must be registered. @@ -337,3 +337,105 @@ In the real world this may look like ``[cache.memcached]``, or ``[database.mysql]`` depending on what the interface label, and handler label's are. Additionally, individual handlers can override their config section by setting ``Meta.config_section``. + + +Overriding Handlers Via Command Line +------------------------------------ + +In some use cases, you will want the end user to have access to override the +default handler of a particular interface. For example, Cement ships with +multiple Output Handlers including ``json``, ``yaml``, and ``mustache``. A +typical application might default to using ``mustache`` to render console +output from text templates. That said, without changing any code in the +application, the end user can simply pass the ``-o json`` command line +option and output the same data that is rendered to template, out in pure +JSON. + +The only built-in handler override that Cement includes is for the above +mentioned example, but you can add any that your application requires. + +The following example shows this in action... note that the following is +already setup by Cement, but we're putting it here for clarity: + +.. code-block:: python + + from cement.core.foundation import CementApp + + class MyApp(CementApp): + class Meta: + label = 'myapp' + + # define what extensions we want to load + extensions = ['mustache', 'json', 'yaml'] + + # define our default output handler + output_handler = 'mustache' + + # define our handler override options + handler_override_options = dict( + output = (['-o'], dict(help='output format')), + ) + + + # create the app + app = MyApp() + + try: + # setup the app + app.setup() + + # define some data for the output handler + data = dict(foo='bar') + + # run the app + app.run() + + # render something using our output handlers, using mustache by + # default which use the default.m template + app.render(data, 'default.m') + + finally: + # close the app + app.close() + + +Note what we see at command line: + +.. code-block:: text + + $ python myapp.py --help + usage: myapp.py [-h] [--debug] [--quiet] [-o {yaml,json}] + + optional arguments: + -h, --help show this help message and exit + --debug toggle debug output + --quiet suppress all output + -o {yaml,json} output format + + +Notice the ``-o`` command line option, that includes the choices: ``yaml`` +and ``json``. This feature will include all Output Handlers that have the +``overridable`` meta-data option set to ``True``. The MustacheOutputHandler +does not set this option, therefore it does not show up as a valid choice. + +Now what happens when we run it? + +.. code-block:: text + + $ python myapp.py + + This text is being rendered via Mustache. + The value of the 'foo' variable is => 'bar' + +The above is the default output, using ``mustache`` as our ``output_handler``, +and rendering the output text from a template called ``default.m``. We can +now override the output handler using the ``-o`` option and modify the output +format: + +.. code-block:: text + + $ python myapp.py -o json + {"foo": "bar"} + + +Again, any handler can be overridden in this fashion. diff --git a/doc/source/dev/output.rst b/doc/source/dev/output.rst index 4dbd23be..e0ba31d7 100644 --- a/doc/source/dev/output.rst +++ b/doc/source/dev/output.rst @@ -160,11 +160,11 @@ And this looks like: The value of the 'foo' variable is => 'bar' -Optionally, we can use the ``JsonOutputHandler`` via ``--json`` to trigger +Optionally, we can use the ``JsonOutputHandler`` via ``-o json`` to trigger just Json output (supressing all other output) using our return dictionary: .. code-block:: text - $ python myapp.py --json + $ python myapp.py -o json {"foo": "bar"} diff --git a/doc/source/examples/load_extensions_via_config.rst b/doc/source/examples/load_extensions_via_config.rst index e0c3116a..a0fd9b76 100644 --- a/doc/source/examples/load_extensions_via_config.rst +++ b/doc/source/examples/load_extensions_via_config.rst @@ -73,12 +73,13 @@ Which looks like: MyApp Does Amazing Things optional arguments: - -h, --help show this help message and exit - --debug toggle debug output - --quiet suppress all output - --json toggle json output handler - --yaml toggle yaml output handler + -h, --help show this help message and exit + --debug toggle debug output + --quiet suppress all output + -o {json,yaml} output format -Note the ``--json`` and ``--yaml`` configuration options that are provided -by the loaded extensions. +Note the ``-o`` command line option that are provided by Cement allowing the +end user to override the output handler with the available/loaded extensions +(that support this feature). + diff --git a/doc/source/upgrading.rst b/doc/source/upgrading.rst index 8988aaa2..66c87c88 100644 --- a/doc/source/upgrading.rst +++ b/doc/source/upgrading.rst @@ -12,6 +12,40 @@ Upgrading from 2.2.x to 2.4.x Cement 2.4 introduced a few incompatible changes from the previous 2.2 stable release, as noted in the :ref:`Changelog `. + +error: unrecognized arguments: --json/--yaml +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +After upgrading to Cement > 2.3.2 you might encounter the error: + +.. code-block:: text + + error: unrecognized arguments: --json + + +Or similar errors like: + +.. code-block:: text + + error: unrecognized arguments: --yaml + + +This is due to a design change, and a new feature allowing the end user to +optionally override handlers via command line. Rather than having a unique +option for every type of output handler, you now have one option that allows +overriding the defined output handler by passing it the handler label. + +Note that only handlers that have ``overridable = True`` in their meta-data +will be valid options. + +To resolve this issue, you simply need to pass ``-o json`` or ``-o yaml`` at +command line to override the default output handler. + +Related: + + * :issue:`229` + + NoSectionError: No section: 'log' ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -33,6 +67,11 @@ The necessary change to resolve this issue is to change all references of ``log`` in relation to the log configuration section, to ``log.logging``. +Related: + + * :issue:`227` + + TypeError: load() takes no arguments (1 given) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/core/foundation_tests.py b/tests/core/foundation_tests.py index 3c544765..75eacd55 100644 --- a/tests/core/foundation_tests.py +++ b/tests/core/foundation_tests.py @@ -2,6 +2,7 @@ import os import sys +import json from cement.core import foundation, exc, backend, config, extension, plugin from cement.core import log, output, handler, hook, arg, controller from cement.utils import test @@ -46,6 +47,7 @@ def my_hook_three(app): class FoundationTestCase(test.CementCoreTestCase): def setUp(self): + super(FoundationTestCase, self).setUp() self.app = self.make_app('my_app') def test_argv_is_none(self): @@ -139,6 +141,34 @@ class FoundationTestCase(test.CementCoreTestCase): app.output = None app.render(dict(foo='bar')) + def test_render_out_to_file(self): + self.app = self.make_app(APP, extensions=['json'], + output_handler='json') + self.app.setup() + self.app.run() + + f = open(self.tmp_file, 'w') + self.app.render(dict(foo='bar'), out=f) + f.close() + + f = open(self.tmp_file, 'r') + data = json.load(f) + f.close() + + self.eq(data, dict(foo='bar')) + + @test.raises(TypeError) + def test_render_bad_out(self): + self.app.setup() + self.app.run() + + try: + self.app.render(dict(foo='bar'), out='bogus type') + except TypeError as e: + self.eq(e.args[0], "Argument 'out' must be a 'file' like object") + raise + + @test.raises(exc.FrameworkError) def test_bad_label(self): try: @@ -171,7 +201,6 @@ class FoundationTestCase(test.CementCoreTestCase): def test_lay_cement(self): app = self.make_app('test', argv=['--quiet']) - app = self.make_app('test', argv=['--json', '--yaml']) def test_none_member(self): class Test(object): @@ -270,3 +299,42 @@ class FoundationTestCase(test.CementCoreTestCase): except AssertionError as e: self.eq(e.args[0], "Invalid exit status code (must be integer)") raise + + def test_handler_override_options(self): + app = self.make_app(APP, + argv=['-o', 'json'], + extensions=['yaml', 'json'], + ) + app.setup() + app.run() + self.eq(app._meta.output_handler, 'json') + + def test_handler_override_options_is_none(self): + app = self.make_app(APP, + core_handler_override_options=None, + handler_override_options=None + ) + app.setup() + app.run() + + def test_handler_override_invalid_interface(self): + app = self.make_app(APP, + handler_override_options=dict( + bogus_interface = (['-f'], ['--foo'], {}), + ) + ) + app.setup() + app.run() + + def test_handler_override_options_not_passed(self): + app = self.make_app(APP, + extensions=['yaml', 'json'], + ) + app.setup() + app.run() + + def test_suppress_output_while_debug(self): + app = self.make_app(APP, debug=True) + app.setup() + app._suppress_output() + diff --git a/tests/ext/json_tests.py b/tests/ext/json_tests.py index f8657e92..0ac83a4c 100644 --- a/tests/ext/json_tests.py +++ b/tests/ext/json_tests.py @@ -5,6 +5,10 @@ import sys from tempfile import mkstemp from cement.core import handler, backend, hook from cement.utils import test +from cement.utils.misc import rando + +APP = rando()[:12] + class JsonExtTestCase(test.CementExtTestCase): CONFIG = '''{ @@ -38,7 +42,7 @@ class JsonExtTestCase(test.CementExtTestCase): output_handler='json', config_handler='json', config_files = [self.tmppath], - argv=['--json'] + argv=['-o', 'json'] ) def test_json(self): @@ -67,3 +71,13 @@ class JsonExtTestCase(test.CementExtTestCase): self.eq(self.app.config.get_section_dict('section'), self.CONFIG_PARSED['section']) + + def test_handler_override_options_is_none(self): + app = self.make_app(APP, + extensions=['json'], + core_handler_override_options={}, + handler_override_options={} + ) + app.setup() + app.run() + app.render(dict(foo='bar')) diff --git a/tests/ext/yaml_tests.py b/tests/ext/yaml_tests.py index 1578fe86..14975e2f 100644 --- a/tests/ext/yaml_tests.py +++ b/tests/ext/yaml_tests.py @@ -6,6 +6,9 @@ import yaml from tempfile import mkstemp from cement.core import handler, hook from cement.utils import test +from cement.utils.misc import rando + +APP = rando()[:12] class YamlExtTestCase(test.CementTestCase): @@ -41,7 +44,7 @@ class YamlExtTestCase(test.CementTestCase): config_handler='yaml', output_handler='yaml', config_files = [self.tmppath], - argv=['--yaml'] + argv=['-o', 'yaml'] ) def tearDown(self): @@ -74,3 +77,13 @@ class YamlExtTestCase(test.CementTestCase): self.eq(self.app.config.get_section_dict('section'), self.CONFIG_PARSED['section']) + + def test_handler_override_options_is_none(self): + app = self.make_app(APP, + extensions=['yaml'], + core_handler_override_options=None, + handler_override_options=None + ) + app.setup() + app.run() + app.render(dict(foo='bar'))