Skip to content

Commit

Permalink
Partially Resolves Issue #370
Browse files Browse the repository at this point in the history
  • Loading branch information
derks committed Jul 13, 2016
1 parent 91045c8 commit 08ca76e
Show file tree
Hide file tree
Showing 12 changed files with 337 additions and 0 deletions.
1 change: 1 addition & 0 deletions ChangeLog
Expand Up @@ -42,6 +42,7 @@ Bugs:
Features:

* :issue:`350` - Support for plugin directories
* :issue:`370` - Handlebars templating support
* :pr:`371` - Jinja2 templating support
* :issue:`373` - Switch over to using Flake8 for PEP8 and style compliance
* :pr:`375` - Redis cache handler support
Expand Down
253 changes: 253 additions & 0 deletions cement/ext/ext_handlebars.py
@@ -0,0 +1,253 @@
"""
The Handlebars Extension provides output templating based on the
`Handlebars Templating Language <https://github.com/wbond/pybars3>`_.
Requirements
------------
* pybars3 (``pip install pybars3``)
Configuration
-------------
Application Meta-data
^^^^^^^^^^^^^^^^^^^^^
This extension supports the following application meta-data via
``CementApp.Meta``:
* **handlebars_helpers** - A dictionary of helper functions to register
with the compiler. Will be merged in with
``HandlebarsOutputHandler.Meta.helpers``.
* **handlebars_partials** - A list of partials (template file names) to
search for, and pre-load before rendering templates. Will be merged in
with ``HandlebarsOutputHandler.Meta.partials``.
Template Directories
^^^^^^^^^^^^^^^^^^^^
To **prepend** a directory to the ``template_dirs`` list defined by the
application/developer, an end-user can add the configuration option
``template_dir`` to their application configuration file under the main
config section:
.. code-block:: text
[myapp]
template_dir = /path/to/my/templates
Usage
-----
.. code-block:: python
class MyApp(CementApp):
class Meta:
label = 'myapp'
extensions = ['handlebars']
output_handler = 'handlebars'
template_module = 'myapp.templates'
template_dirs = [
'~/.myapp/templates',
'/usr/lib/myapp/templates',
]
# ...
Note that the above ``template_module`` and ``template_dirs`` are the
auto-defined defaults but are added here for clarity. From here, you
would then put a Handlebars template file in
``myapp/templates/my_template.handlebars`` or
``/usr/lib/myapp/templates/my_template.handlebars`` and then render a data
dictionary with it:
.. code-block:: python
app.render(some_data, 'my_template.handlebars')
Helpers
^^^^^^^
Custom helper functions can easily be registered with the compiler via
``CementApp.Meta.handlebars_helpers`` and/or
``HandlebarsOutputHandler.Meta.helpers``.
.. code-block:: python
def my_custom_helper(this, arg1, arg2):
# do something with arg1 and arg2
if arg1 == arg2:
return True
else:
return False
class MyApp(CementApp):
class Meta:
label = 'myapp'
extensions = ['handlebars']
output_handler = 'handlebars'
handlebars_helpers = {
'myhelper' : my_custom_helper
}
# ...
You would then access this in your template as:
.. code-block:: console
This is my template
{{#if (myhelper this that)}}
This will only appear if myhelper returns True
{{/if}}
See the `Handlebars Documentation <https://github.com/wbond/pybars3>`_ for
more information on helpers.
Partials
^^^^^^^^
Though partials are supported by the library, there is no good way of
automatically loading them in the context and workflow of a typical Cement
application. Therefore, the extension needs a list of partial
template names to know what to preload, in order to make partials work.
Future versions will hopefully automate this.
Example:
.. code-block:: python
class MyApp(CementApp):
class Meta:
label = 'myapp'
extensions = ['handlebars']
output_handler = 'handlebars'
handlebars_partials = [
'header.bars',
'footer.bars',
]
Where ``header.bars`` and ``footer.bars`` are template names that will be
searched for, and loaded just like other templates loaded from template dirs.
These are then referenced in templates as:
.. code-block:: console
{{> "header.bars"}}
This is my template
{{> "footer.bars}}
See the `Handlebars Documentation <https://github.com/wbond/pybars3>`_ for
more information on partials.
"""

import sys

# Monkey patch so we don't escape HTML (not clear how else to do this)
# See: https://github.com/wbond/pybars3/issues/25
import pybars._compiler

original_prepare = pybars._compiler.prepare
def my_prepare(value, escape):
return original_prepare(value, False)
pybars._compiler.prepare = my_prepare

from pybars import Compiler
from cement.core import output, exc, handler
from cement.utils.misc import minimal_logger


LOG = minimal_logger(__name__)


class HandlebarsOutputHandler(output.TemplateOutputHandler):

"""
This class implements the :ref:`IOutput <cement.core.output>`
interface. It provides text output from template and uses the
`Handlebars Templating Language <http://handlebarsjs.com/>`_ for Python
via the ``pybars`` library. Please see the developer documentation on
:ref:`Output Handling <dev_output_handling>`.
**Note** This extension has an external dependency on ``pybars3``. You
must include ``pybars3`` in your applications dependencies as Cement
explicitly does **not** include external dependencies for optional
extensions.
"""

class Meta:

"""Handler meta-data."""

interface = output.IOutput
label = 'handlebars'

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

#: Custom helpers
helpers = {}

#: List of partials to preload
partials = []

def __init__(self, *args, **kw):
super(HandlebarsOutputHandler, self).__init__(*args, **kw)
self._raw_partials = {}

def _setup(self, app):
super(HandlebarsOutputHandler, self)._setup(app)
if hasattr(self.app._meta, 'handlebars_helpers'):
self._meta.helpers = self.app._meta.handlebars_helpers
#self._meta.helpers.update(self.app._meta.handlebars_helpers)
if hasattr(self.app._meta, 'handlebars_partials'):
#self._meta.partials = self._meta.partials + \
self._meta.partials = self.app._meta.handlebars_partials
for partial in self._meta.partials:
self._raw_partials[partial] = self.load_template(partial)

def render_content(self, data, content):
bars = Compiler()
if sys.version_info[0] >= 3:
if not isinstance(content, str):
content = content.decode('utf-8')
else:
if not isinstance(content, unicode): # pragma: nocover # noqa
content = content.decode('utf-8') # pragma: nocover
content = bars.compile(content)

# need to render partials
partials = {}
for key,val in self._raw_partials.items():
partials[key] = bars.compile(val)

return content(data, helpers=self._meta.helpers, partials=partials)

def render(self, data, template):
"""
Take a data dictionary and render it using the given template file.
Required Arguments:
:param data: The data dictionary to render.
: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)
"""
LOG.debug("rendering output using '%s' as a template." % template)
res = self.render_content(data, self.load_template(template))
return res


def load(app):
handler.register(HandlebarsOutputHandler)
9 changes: 9 additions & 0 deletions doc/source/api/ext/ext_handlebars.rst
@@ -0,0 +1,9 @@
.. _cement.ext.ext_handlebars:

:mod:`cement.ext.ext_handlebars`
--------------------------------

.. automodule:: cement.ext.ext_handlebars
:members:
:private-members:
:show-inheritance:
1 change: 1 addition & 0 deletions doc/source/api/index.rst
Expand Up @@ -56,6 +56,7 @@ Cement Extension Modules
ext/ext_daemon
ext/ext_dummy
ext/ext_genshi
ext/ext_handlebars
ext/ext_jinja2
ext/ext_json
ext/ext_json_configobj
Expand Down
1 change: 1 addition & 0 deletions doc/source/dev/output.rst
Expand Up @@ -20,6 +20,7 @@ The following output handlers are included and maintained with Cement:
* :class:`cement.ext.ext_json.JsonOutputHandler`
* :class:`cement.ext.ext_yaml.YamlOutputHandler`
* :class:`cement.ext.ext_genshi.GenshiOutputHandler`
* :class:`cement.ext.ext_genshi.HandlebarsOutputHandler`
* :class:`cement.ext.ext_jinja2.Jinja2OutputHandler`
* :class:`cement.ext.ext_mustache.MustacheOutputHandler`
* :class:`cement.ext.ext_tabulate.TabulateOutputHandler`
Expand Down
1 change: 1 addition & 0 deletions requirements-dev-py3.txt
Expand Up @@ -17,3 +17,4 @@ pylibmc
redis
jinja2
watchdog
pybars3
1 change: 1 addition & 0 deletions requirements-dev.txt
Expand Up @@ -20,3 +20,4 @@ colorlog
tabulate
jinja2
watchdog
pybars3
67 changes: 67 additions & 0 deletions tests/ext/handlebars_tests.py
@@ -0,0 +1,67 @@
"""Tests for cement.ext.ext_handlebars."""

import sys
import random

from cement.core import exc, foundation, handler, backend, controller
from cement.utils import test

class HandlebarsTestApp(test.TestApp):
class Meta:
extensions = ['handlebars']
output_handler = 'handlebars'
handlebars_helpers = {}
#handlebars_partials = ['test_partial_template.handlebars']

class HandlebarsExtTestCase(test.CementExtTestCase):
app_class = HandlebarsTestApp

# def setUp(self):
# super(HandlebarsExtTestCase, self).setUp()
# self.reset_backend()
# self.app = self.make_app('tests',
# extensions=['handlebars'],
# output_handler='handlebars',
# argv=[]
# )

def test_handlebars(self):
self.app.setup()
rando = random.random()
res = self.app.render(dict(foo=rando), 'test_template.handlebars')
handlebars_res = "foo equals %s\n" % rando
self.eq(res, handlebars_res)

def test_handlebars_partials(self):
# FIX ME: Not sure what's going on here
self.app.setup()

rando = random.random()
res = self.app.render(dict(foo=rando), 'test_base_template.handlebars')
handlebars_res = "Inside partial > foo equals %s\n" % rando
self.eq(res, handlebars_res)

@test.raises(exc.FrameworkError)
def test_handlebars_bad_template(self):
self.app.setup()
res = self.app.render(dict(foo='bar'), 'bad_template2.handlebars')

@test.raises(exc.FrameworkError)
def test_handlebars_nonexistent_template(self):
self.app.setup()
res = self.app.render(dict(foo='bar'), 'missing_template.handlebars')

@test.raises(exc.FrameworkError)
def test_handlebars_none_template(self):
self.app.setup()
try:
res = self.app.render(dict(foo='bar'), None)
except exc.FrameworkError as e:
self.eq(e.msg, "Invalid template path 'None'.")
raise

@test.raises(exc.FrameworkError)
def test_handlebars_bad_module(self):
self.app.setup()
self.app._meta.template_module = 'this_is_a_bogus_module'
res = self.app.render(dict(foo='bar'), 'bad_template.handlebars')
Empty file.
1 change: 1 addition & 0 deletions tests/templates/test_base_template.handlebars
@@ -0,0 +1 @@
{{> "test_partial_template.handlebars"}}
1 change: 1 addition & 0 deletions tests/templates/test_partial_template.handlebars
@@ -0,0 +1 @@
Inside partial > foo equals {{foo}}
1 change: 1 addition & 0 deletions tests/templates/test_template.handlebars
@@ -0,0 +1 @@
foo equals {{foo}}

0 comments on commit 08ca76e

Please sign in to comment.