Resolves Issue #395

This commit is contained in:
BJ Dierkes 2016-07-14 14:47:38 -05:00
parent 1f98a434e6
commit d17ff65c67
6 changed files with 154 additions and 11 deletions

View File

@ -51,6 +51,8 @@ Features:
* :issue:`389` - ConfigObj support for Python 3
* :issue:`394` - Watchdog extension for cross-platform filesystem event
monitoring
* :issue:`395` - Ability to pass metadata keyword arguments to handlers
via ``CementApp.Meta.meta_defaults``.
Refactoring;

View File

@ -416,6 +416,32 @@ class CementApp(meta.MetaMixin):
config_defaults = None
"""Default configuration dictionary. Must be of type 'dict'."""
meta_defaults = {}
"""
Default metadata dictionary used to pass high level options from the
application down to handlers at the point they are registered by the
framework **if the handler has not already been instantiated**.
For example, if requiring the ``json`` extension, you might want to
override ``JsonOutputHandler.Meta.json_module`` with ``ujson`` by
doing the following
.. code-block:: python
from cement.core.foundation import CementApp
from cement.utils.misc import init_defaults
META = init_defaults('output.json')
META['output.json']['json_module'] = 'ujson'
class MyApp(CementApp):
class Meta:
label = 'myapp'
extensions = ['json']
meta_defaults = META
"""
catch_signals = SIGNALS
"""
List of signals to catch, and raise exc.CaughtSignal for.
@ -1170,7 +1196,17 @@ class CementApp(meta.MetaMixin):
self.catch_signal(signum)
def _resolve_handler(self, handler_type, handler_def, raise_error=True):
han = self.handler.resolve(handler_type, handler_def, raise_error)
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,
raise_error=raise_error,
meta_defaults=meta_defaults)
if han is not None:
han._setup(self)
return han

View File

@ -256,7 +256,7 @@ class HandlerManager(object):
return False
def resolve(self, handler_type, handler_def, raise_error=True):
def resolve(self, handler_type, handler_def, **kwargs):
"""
Resolves the actual handler, as it can be either a string identifying
the handler to load from self.__handlers__, or it can be an
@ -265,9 +265,12 @@ class HandlerManager(object):
:param handler_type: The type of handler (aka the interface label)
:param handler_def: The handler as defined in CementApp.Meta.
:type handler_def: str, uninstantiated object, or instantiated object
:param raise_error: Whether or not to raise an exception if unable
:keyword raise_error: Whether or not to raise an exception if unable
to resolve the handler.
:type raise_error: boolean
:keywork meta_defaults: Optional meta-data dictionary used as
defaults to pass when instantiating uninstantiated handlers. See
``CementApp.Meta.meta_defaults``.
:returns: The instantiated handler object.
Usage:
@ -284,15 +287,18 @@ class HandlerManager(object):
log = app.handler.resolve('log', ColorLogHandler())
"""
raise_error = kwargs.get('raise_error', True)
meta_defaults = kwargs.get('meta_defaults', {})
han = None
if type(handler_def) == str:
han = self.get(handler_type, handler_def)()
han = self.get(handler_type, handler_def)(**meta_defaults)
elif hasattr(handler_def, '_meta'):
if not self.registered(handler_type, handler_def._meta.label):
self.register(handler_def.__class__)
han = handler_def
elif hasattr(handler_def, 'Meta'):
han = handler_def()
han = handler_def(**meta_defaults)
if not self.registered(handler_type, han._meta.label):
self.register(handler_def)

View File

@ -44,6 +44,7 @@ class CementTestCase(unittest.TestCase):
super(CementTestCase, self).__init__(*args, **kw)
self.tmp_file = None
self.tmp_dir = None
self.rando = None
def setUp(self):
"""
@ -59,6 +60,10 @@ class CementTestCase(unittest.TestCase):
_, self.tmp_file = mkstemp(prefix=_prefix)
self.tmp_dir = mkdtemp(prefix=_prefix)
# create a random string for each test (useful to verify things
# uniquely so every test isn't using the same "My Test String")
self.rando = rando()[:12]
def tearDown(self):
"""
Tears down the test environment (if necessary), removes any temporary

View File

@ -63,15 +63,99 @@ Related:
* :issue:`394`
Extensions
^^^^^^^^^^
Ability To Pass Meta Defaults From CementApp.Meta Down To Handlers
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* :ref:`Jinja2 <cement.ext.ext_jinja2>` - Provides template support using
the Jinja2 language.
Cement handlers are often referenced by their label, and not passed as
pre-instantiated objects which requires the framework to instantiate them
dynamically with no keyword arguments.
For example:
.. code-block:: python
from cement.core.foundation import CementApp
class MyApp(CementApp):
class Meta:
label = 'myapp'
extensions = ['json']
In the above, Cement will load the ``json`` extension, which
registers ``JsonOutputHandler``. When it comes time to recall that handler,
it is looked up as ``output.json`` where ``output`` is the handler type
(interface) and ``json`` is the handler label. The class is then instantiated
without any arguments or keyword arguments before use. If a developer needed
to override any meta options in ``JsonOutputHandler.Meta`` they would
**previously** have had to sub-class it. Consider the following example,
where we sub-class ``JsonOutputHandler`` in order to override
``JsonOutputHandler.Meta.json_module``:
.. code-block:: python
from cement.core.foundation import CementApp
from cement.ext.ext_json import JsonOutputHandler
class MyJsonOutputHandler(JsonOutputHandler):
class Meta:
json_module = 'ujson'
def override_json_output_handler(app):
app.handler.register(MyJsonOutputHandler, force=True)
class MyApp(CementApp):
class Meta:
label = 'myapp'
extensions = ['json']
hooks = [
('post_setup', override_json_output_handler)
]
If there were anything else in the ``JsonOutputHandler`` that the developer
needed to subclass, this would be fine. However the purpose of the above is
soley to override ``JsonOutputHandler.Meta.json_module``, which is tedious.
As of Cement 2.9, the above can be accomplished more-easily by the following
by way of ``CementApp.Meta.meta_defaults`` (similar to how ``config_defaults``
are handled:
.. code-block:: python
from cement.core.foundation import CementApp
from cement.utils.misc import init_defaults
META = init_defaults('output.json')
META['output.json']['json_module'] = 'ujson'
class MyApp(CementApp):
class Meta:
label = 'myapp'
extensions = ['json']
output_handler = 'json'
meta_defaults = META
When ``JsonOutputHandler`` is instantiated, the defaults from
``META['output.json']`` will be passed as ``**kwargs`` (overriding builtin
meta options).
Related:
* :issue:`395`
Additional Extensions
^^^^^^^^^^^^^^^^^^^^^
* :ref:`Jinja2 <cement.ext.ext_jinja2>` - Provides template based output
handling using the Jinja2 templating language
* :ref:`Redis <cement.ext.ext_redis>` - Provides caching support using
Redis backend.
Redis backend
* :ref:`Watchdog <cement.ext.ext_watchdog>` - Provides cross-platform
filesystem event monitoring using Watchdog library.
filesystem event monitoring using the Watchdog library.
* :ref:`Handlebars <cement.ext.ext_handlebars>` - Provides template based
output handling using the Handlebars templating language

View File

@ -535,3 +535,13 @@ class FoundationTestCase(test.CementCoreTestCase):
app.__import__('time')
app.__import__('sleep', from_module='time')
def test_meta_defaults(self):
DEBUG_FORMAT = "TEST DEBUG FORMAT - %s" % self.rando
META = {}
META['log.logging'] = {}
META['log.logging']['debug_format'] = DEBUG_FORMAT
app = self.make_app(meta_defaults=META)
app.setup()
self.eq(app.log._meta.debug_format, DEBUG_FORMAT)