Skip to content

Commit

Permalink
Resolves Issues #322 and #332
Browse files Browse the repository at this point in the history
  • Loading branch information
derks committed Oct 5, 2015
1 parent f9a5f27 commit e6092d9
Show file tree
Hide file tree
Showing 19 changed files with 305 additions and 21 deletions.
4 changes: 3 additions & 1 deletion ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Features:
* :issue:`205` - Added new ``ArgparseController`` and ``expose`` decorator
in ``ext_argparse`` to eventually replace ``CementBaseController``.
* :issue:`299` - Added Argcomplete Framework Extension
* :issue:`322` - Added Tabulate Framework Extension (tabulatized output)

Refactoring:

Expand All @@ -51,7 +52,8 @@ Incompatible:

* :issue:`313` - Default ``Cement.Meta.exit_on_close`` to ``False``.
Calls to ``sys.exit()`` should be explicit by the app developer, and not implied by the framework.

* :issue:`332` - Output Handler Interface Must Support Keyword Arguments


2.6.0 - Thu May 14, 2015
------------------------------------------------------------------------------
Expand Down
6 changes: 4 additions & 2 deletions cement/core/foundation.py
Original file line number Diff line number Diff line change
Expand Up @@ -807,7 +807,7 @@ def close(self, code=None):
if self._meta.exit_on_close is True:
sys.exit(self.exit_code)

def render(self, data, template=None, out=sys.stdout):
def render(self, data, template=None, out=sys.stdout, **kw):
"""
This is a simple wrapper around self.output.render() which simply
returns an empty string if no self.output handler is defined.
Expand All @@ -826,11 +826,13 @@ def render(self, data, template=None, out=sys.stdout):
else:
data = res

kw['template'] = template

if self.output is None:
LOG.debug('render() called, but no output handler defined.')
out_text = ''
else:
out_text = self.output.render(data, template)
out_text = self.output.render(data, **kw)

for res in hook.run('post_render', self, out_text):
if not type(res) is str:
Expand Down
6 changes: 4 additions & 2 deletions cement/core/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,11 @@ def _setup(app_obj):
"""

def render(data_dict):
def render(data_dict, *args, **kwargs):
"""
Render the data_dict into output in some fashion.
Render the data_dict into output in some fashion. This function must
access both ``*args`` and ``**kwargs`` to allow an application to mix
output handlers that support different features.
:param data_dict: The dictionary whose data we need to render into
output.
Expand Down
7 changes: 5 additions & 2 deletions cement/ext/ext_dummy.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,12 @@ class Meta:
label = 'dummy'
"""The string identifier of this handler."""

display_override_option = False
#: Whether or not to include ``dummy`` as an available to choice
#: to override the ``output_handler`` via command line options.
overridable = False

def render(self, data_dict, template=None):

def render(self, data_dict, template=None, **kw):
"""
This implementation does not actually render anything to output, but
rather logs it to the debug facility.
Expand Down
6 changes: 4 additions & 2 deletions cement/ext/ext_genshi.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,19 +78,21 @@ class Meta:
interface = output.IOutput
label = 'genshi'

def render(self, data_dict, template):
def render(self, data_dict, **kw):
"""
Take a data dictionary and render it using the given template file.
Required Arguments:
:param data_dict: The data dictionary to render.
:param template: The path to the template, after the
:keyword template: The path to the template, after the
``template_module`` or ``template_dirs`` prefix as defined in the
application.
:returns: str (the rendered template text)
"""
template = kw.get('template', None)

LOG.debug("rendering output using '%s' as a template." % template)
content = self.load_template(template)
tmpl = NewTextTemplate(content)
Expand Down
4 changes: 3 additions & 1 deletion cement/ext/ext_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,14 @@ class Meta:
label = 'json'
"""The string identifier of this handler."""

#: Whether or not to include ``json`` as an available to choice
#: to override the ``output_handler`` via command line options.
overridable = True

def __init__(self, *args, **kw):
super(JsonOutputHandler, self).__init__(*args, **kw)

def render(self, data_dict, template=None):
def render(self, data_dict, **kw):
"""
Take a data dictionary and render it as Json output. Note that the
template option is received here per the interface, however this
Expand Down
11 changes: 8 additions & 3 deletions cement/ext/ext_mustache.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,24 +117,29 @@ class Meta:
interface = output.IOutput
label = 'mustache'

#: Whether or not to include ``mustache`` as an available to choice
#: to override the ``output_handler`` via command line options.
overridable = False

def __init__(self, *args, **kw):
super(MustacheOutputHandler, self).__init__(*args, **kw)
self._partials_loader = PartialsLoader(self)

def render(self, data_dict, template):
def render(self, data_dict, **kw):
"""
Take a data dictionary and render it using the given template file.
Required Arguments:
:param data_dict: The data dictionary to render.
:param template: The path to the template, after the
:keyword template: The path to the template, after the
``template_module`` or ``template_dirs`` prefix as defined in the
application.
:returns: str (the rendered template text)
"""

template = kw.get('template', None)

LOG.debug("rendering output using '%s' as a template." % template)
content = self.load_template(template)
stache = Renderer(partials=self._partials_loader)
Expand Down
142 changes: 142 additions & 0 deletions cement/ext/ext_tabulate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
"""Tabulate extension module."""

import sys
from tabulate import tabulate
from ..core import output, exc, handler
from ..utils.misc import minimal_logger

LOG = minimal_logger(__name__)


class TabulateOutputHandler(output.CementOutputHandler):

"""
This class implements the :ref:`IOutput <cement.core.output>`
interface. It provides tabularized text output using the
`Tabulate <https://pypi.python.org/pypi/tabulate>`_ module. Please
see the developer documentation on
:ref:`Output Handling <dev_output_handling>`.
**Note** This extension has an external dependency on ``tabulate``. You
must include ``tabulate`` in your applications dependencies as Cement
explicitly does **not** include external dependencies for optional
extensions.
Usage:
.. code-block:: python
from cement.core import foundation
class MyApp(foundation.CementApp):
class Meta:
label = 'myapp'
extensions = ['tabulate']
output_handler = 'tabulate'
# ...
Usage:
.. code-block:: python
# create a dataset
headers = ['NAME', 'AGE', 'ADDRESS']
data = [
["Krystin Bartoletti", 47, "PSC 7591, Box 425, APO AP 68379"],
["Cris Hegan", 54, "322 Reubin Islands, Leylabury, NC 34388"],
["George Champlin", 25, "Unit 6559, Box 124, DPO AA 25518"],
]
# via the app object
myapp.render(data, headers=headers)
# or from within a controller or handler
self.app.render(data, headers=headers)
Looks like:
.. code-block:: console
| NAME | AGE | ADDRESS |
|--------------------+-----+-----------------------------------------|
| Krystin Bartoletti | 47 | PSC 7591, Box 425, APO AP 68379 |
| Cris Hegan | 54 | 322 Reubin Islands, Leylabury, NC 34388 |
| George Champlin | 25 | Unit 6559, Box 124, DPO AA 25518 |
Configuration:
This extension does not support any configuration file settings.
"""

class Meta:

"""Handler meta-data."""

interface = output.IOutput
label = 'tabulate'

#: Whether or not to pad the output with an extra pre/post '\n'
padding = True

#: Default template format. See the ``tabulate`` documentation for
#: all supported template formats.
format = 'orgtbl'

#: Default headers to use.
headers = []

#: Default alignment for string columns. See the ``tabulate``
#: documentation for all supported ``stralign`` options.
string_alignment = 'left'

#: Default alignment for numeric columns. See the ``tabulate``
#: documentation for all supported ``numalign`` options.
numeric_alignment = 'decimal'

#: String format to use for float values.
float_format = 'g'

#: Default replacement for missing value.
missing_value = ''

#: Whether or not to include ``tabulate`` as an available to choice
#: to override the ``output_handler`` via command line options.
overridable = False



def render(self, data, **kw):
"""
Take a data dictionary and render it into a table. Additional
keyword arguments are passed directly to ``tabulate.tabulate``.
Required Arguments:
:param data_dict: The data dictionary to render.
:returns: str (the rendered template text)
"""
headers = kw.get('headers', self._meta.headers)

out = tabulate(data, headers,
tablefmt=kw.get('tablefmt', self._meta.format),
stralign=kw.get('stralign', self._meta.string_alignment),
numalign=kw.get('numalign', self._meta.numeric_alignment),
missingval=kw.get('missingval', self._meta.missing_value),
floatfmt=kw.get('floatfmt', self._meta.float_format),
)
out = out + '\n'

if self._meta.padding is True:
out = '\n' + out + '\n'

return out


def load(app):
handler.register(TabulateOutputHandler)
6 changes: 4 additions & 2 deletions cement/ext/ext_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ class Meta:

interface = output.IOutput
label = 'yaml'

#: Whether or not to include ``yaml`` as an available to choice
#: to override the ``output_handler`` via command line options.
overridable = True

def __init__(self, *args, **kw):
Expand All @@ -90,14 +93,13 @@ def __init__(self, *args, **kw):
def _setup(self, app_obj):
self.app = app_obj

def render(self, data_dict, template=None):
def render(self, data_dict, **kw):
"""
Take a data dictionary and render it as Yaml output. Note that the
template option is received here per the interface, however this
handler just ignores it.
:param data_dict: The data dictionary to render.
:param template: This option is completely ignored.
:returns: A Yaml encoded string.
:rtype: str
Expand Down
9 changes: 9 additions & 0 deletions doc/source/api/ext/ext_tabulate.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.. _cement.ext.ext_tabulate:

:mod:`cement.ext.ext_tabulate`
------------------------------

.. automodule:: cement.ext.ext_tabulate
:members:
:private-members:
:show-inheritance:
1 change: 1 addition & 0 deletions doc/source/api/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,6 @@ Cement Extension Modules
ext/ext_plugin
ext/ext_reload_config
ext/ext_smtp
ext/ext_tabulate
ext/ext_yaml
ext/ext_yaml_configobj
1 change: 1 addition & 0 deletions doc/source/examples/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ with Cement.
bash_auto_completion
arbitrary_extra_arguments
reload_config
tabularized_output
52 changes: 52 additions & 0 deletions doc/source/examples/tabularized_output.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
Tabularized Output
==================

Users familiar with MySQL, PGSQL, etc find comfort in table-based output
patterns. For one it adds structure, and two generally makes things much
more readable. The folloiwing is an example of a simple app using the
:ref:`Tabulate <cement.ext.ext_tabulate>` extension:

.. code-block:: python
from cement.core.foundation import CementApp
from cement.core.controller import expose, CementBaseController
class MyController(CementBaseController):
class Meta:
label = 'base'
@expose(hide=True)
def default(self):
headers=['NAME', 'AGE', 'ADDRESS']
data=[
["Krystin Bartoletti", 47, "PSC 7591, Box 425, APO AP 68379"],
["Cris Hegan", 54, "322 Reubin Islands, Leylabury, NC 34388"],
["George Champlin", 25, "Unit 6559, Box 124, DPO AA 25518"],
]
self.app.render(data, headers=headers)
class MyApp(CementApp):
class Meta:
label = 'myapp'
extensions = ['tabulate']
output_handler = 'tabulate'
handlers = [MyController]
with MyApp() as app:
app.run()
The output looks like:

.. code-block:: console
$ python myapp.py
| NAME | AGE | ADDRESS |
|--------------------+-------+-----------------------------------------|
| Krystin Bartoletti | 47 | PSC 7591, Box 425, APO AP 68379 |
| Cris Hegan | 54 | 322 Reubin Islands, Leylabury, NC 34388 |
| George Champlin | 25 | Unit 6559, Box 124, DPO AA 25518 |
Loading

0 comments on commit e6092d9

Please sign in to comment.