0001"""Buffet templating plugin and render functions
0002
0003The Buffet object is styled after the original Buffet module that implements
0004template language neutral rendering for CherryPy. This version of Buffet also
0005contains caching functionality that utilizes
0006`Beaker middleware <http://beaker.groovie.org/>`_ to provide template language
0007neutral caching functionality.
0008
0009A customized version of
0010`BuffetMyghty <http://projects.dowski.com/projects/buffetmyghty>`_ is included
0011that provides a template API hook as the ``pylonsmyghty`` engine. This version
0012of BuffetMyghty disregards some of the TurboGears API spec so that traditional
0013Myghty template names can be used with ``/`` and file extensions.
0014
0015The render functions are intended as the primary user-visible rendering
0016commands and hook into Buffet to make rendering content easy.
0017"""
0018import logging
0019import os
0020import warnings
0021
0022try:
0023 from cStringIO import StringIO
0024except ImportError:
0025 from StringIO import StringIO
0026import pkg_resources
0027
0028import pylons
0029
0030__all__ = ['Buffet', 'MyghtyTemplatePlugin', 'render', 'render_response']
0031
0032PYLONS_VARS = ['c', 'g', 'h', 'render', 'request', 'session', 'translator',
0033 'ungettext', '_', 'N_']
0034
0035log = logging.getLogger(__name__)
0036
0037class BuffetError(Exception):
0038 """Buffet Exception"""
0039 pass
0040
0041
0042class Buffet(object):
0043 """Buffet style plug-in template rendering
0044
0045 Buffet implements template language plug-in support modeled highly on the
0046 `Buffet Project <http://projects.dowski.com/projects/buffet>`_ from which
0047 this class inherits its name.
0048 """
0049 def __init__(self, default_engine=None, template_root=None,
0050 default_options=None, **config):
0051 """Initialize the Buffet renderer, and optionally set a default
0052 engine/options"""
0053 if default_options is None:
0054 default_options = {}
0055 self.default_engine = default_engine
0056 self.template_root = template_root
0057 self.default_options = default_options
0058 self.engines = {}
0059 log.debug("Initialized Buffet object")
0060 if self.default_engine:
0061 self.prepare(default_engine, template_root, **config)
0062
0063 def prepare(self, engine_name, template_root=None, alias=None, **config):
0064 """Prepare a template engine for use
0065
0066 This method must be run before the `render <#render>`_ method is called
0067 so that the ``template_root`` and options can be set. Template engines
0068 can also be aliased if you wish to use multiplate configurations of the
0069 same template engines, or prefer a shorter name when rendering a
0070 template with the engine of your choice.
0071 """
0072 Engine = available_engines.get(engine_name, None)
0073 if not Engine:
0074 raise TemplateEngineMissing('Please install a plugin for '
0075 '"%s" to use its functionality' % engine_name)
0076 engine_name = alias or engine_name
0077 extra_vars_func = config.pop(engine_name + '.extra_vars_func', None)
0078 self.engines[engine_name] = dict(engine=Engine(extra_vars_func=extra_vars_func,
0080 options=config),
0081 root=template_root)
0082 log.debug("Adding %s template language for use with Buffet",
0083 engine_name)
0084
0085 def _update_names(self, ns):
0086 """Return a dict of Pylons vars and their respective objects updated
0087 with the ``ns`` dict."""
0088 d = dict(
0089 c=pylons.c._current_obj(),
0090 g=pylons.g._current_obj(),
0091 h=pylons.config.get('pylons.h') or pylons.h._current_obj(),
0092 render=render,
0093 request=pylons.request._current_obj(),
0094 translator=pylons.translator,
0095 ungettext=pylons.i18n.ungettext,
0096 _=pylons.i18n._,
0097 N_=pylons.i18n.N_
0098 )
0099
0100 # If the session was overriden to be None, don't populate the session
0101 # var
0102 if pylons.config['pylons.environ_config'].get('session', True):
0103 d['session'] = pylons.session._current_obj()
0104 d.update(ns)
0105 log.debug("Updated render namespace with pylons vars: %s", d)
0106 return d
0107
0108 def render(self, engine_name=None, template_name=None,
0109 include_pylons_variables=True, namespace=None,
0110 cache_key=None, cache_expire=None, cache_type=None, **options):
0111 """Render a template using a template engine plug-in
0112
0113 To use templates it is expected that you will attach data to be used in
0114 the template to the ``c`` variable which is available in the controller
0115 and the template.
0116
0117 When porting code from other projects it is sometimes easier to use an
0118 exisitng dictionary which can be specified with ``namespace``.
0119
0120 ``engine_name``
0121 The name of the template engine to use, which must be
0122 'prepared' first.
0123 ``template_name``
0124 Name of the template to render
0125 ``include_pylons_variables``
0126 If a custom namespace is specified this determines whether Pylons
0127 variables are included in the namespace or not. Defaults to
0128 ``True``.
0129 ``namespace``
0130 A custom dictionary of names and values to be substituted in the
0131 template.
0132
0133 Caching options (uses Beaker caching middleware)
0134
0135 ``cache_key``
0136 Key to cache this copy of the template under.
0137 ``cache_type``
0138 Valid options are ``dbm``, ``file``, ``memory``, or
0139 ``ext:memcached``.
0140 ``cache_expire``
0141 Time in seconds to cache this template with this ``cache_key`` for.
0142 Or use 'never' to designate that the cache should never expire.
0143
0144 The minimum key required to trigger caching is ``cache_expire='never'``
0145 which will cache the template forever seconds with no key.
0146
0147 All other keyword options are passed directly to the template engine
0148 used.
0149 """
0150 if not engine_name and self.default_engine:
0151 engine_name = self.default_engine
0152 engine_config = self.engines.get(engine_name)
0153
0154 if not engine_config:
0155 raise Exception("No engine with that name configured: %s" % engine_name)
0157
0158 full_path = template_name
0159
0160 if engine_name == 'pylonsmyghty':
0161 if namespace is None:
0162 namespace = {}
0163 # Reserved myghty keywords
0164 for key in ('output_encoding', 'encoding_errors',
0165 'disable_unicode'):
0166 if key in namespace:
0167 options[key] = namespace.pop(key)
0168
0169 if include_pylons_variables:
0170 namespace['_global_args'] = self._update_names({})
0171 else:
0172 namespace['_global_args'] = {}
0173
0174 # If they passed in a variable thats listed in the global_args,
0175 # update the global args one instead of duplicating it
0176 interp = engine_config['engine'].interpreter
0177 for key in interp.global_args.keys() + interp.init_params.get('allow_globals', []):
0179 if key in namespace:
0180 namespace['_global_args'][key] = namespace.pop(key)
0181 else:
0182 if namespace is None:
0183 if not include_pylons_variables:
0184 raise BuffetError('You must specify ``namespace`` when '
0185 '``include_pylons_variables`` is False')
0186 else:
0187 namespace = self._update_names({})
0188 elif include_pylons_variables:
0189 namespace = self._update_names(namespace)
0190
0191 if not full_path.startswith(os.path.sep) and not engine_name.startswith('pylons') and not engine_name.startswith('mako') and engine_config['root'] is not None:
0195 full_path = os.path.join(engine_config['root'], template_name)
0196 full_path = full_path.replace(os.path.sep, '.').lstrip('.')
0197
0198 # Don't pass format into the template engine if it's None
0199 if 'format' in options and options['format'] is None:
0200 del options['format']
0201
0202 # If one of them is not None then the user did set something
0203 if cache_key is not None or cache_expire is not None or cache_type is not None:
0205 if not cache_type:
0206 cache_type = 'dbm'
0207 if not cache_key:
0208 cache_key = 'default'
0209 if cache_expire == 'never':
0210 cache_expire = None
0211 def content():
0212 log.debug("Cached render running for %s", full_path)
0213 return engine_config['engine'].render(namespace,
0214 template=full_path, **options)
0215 tfile = full_path
0216 if options.get('fragment', False):
0217 tfile += '_frag'
0218 if options.get('format', False):
0219 tfile += options['format']
0220 log.debug("Using render cache for %s", full_path)
0221 mycache = pylons.cache.get_cache(tfile)
0222 content = mycache.get_value(cache_key, createfunc=content,
0223 type=cache_type, expiretime=cache_expire)
0224 return content
0225
0226 log.debug("Rendering template %s with engine %s", full_path, engine_name)
0227 return engine_config['engine'].render(namespace, template=full_path,
0228 **options)
0229
0230
0231class TemplateEngineMissing(Exception):
0232 """Exception to toss when an engine is missing"""
0233 pass
0234
0235
0236class MyghtyTemplatePlugin(object):
0237 """Myghty Template Plugin
0238
0239 This Myghty Template Plugin varies from the official BuffetMyghty in that
0240 it will properly populate all the default Myghty variables needed and
0241 render fragments.
0242 """
0243 extension = "myt"
0244
0245 def __init__(self, extra_vars_func=None, options=None):
0246 """Initialize Myghty template engine"""
0247 if options is None:
0248 options = {}
0249 myt_opts = {}
0250 for k, v in options.iteritems():
0251 if k.startswith('myghty.'):
0252 myt_opts[k[7:]] = v
0253 import myghty.interp
0254 self.extra_vars = extra_vars_func
0255 self.interpreter = myghty.interp.Interpreter(**myt_opts)
0256
0257 def load_template(self, template_path):
0258 """Unused method for TG plug-in API compatibility"""
0259 pass
0260
0261 def render(self, info, format="html", fragment=False, template=None,
0262 output_encoding=None, encoding_errors=None,
0263 disable_unicode=None):
0264 """Render the template indicated with info as the namespace and globals
0265 from the ``info['_global_args']`` key."""
0266 buf = StringIO()
0267 global_args = info.pop('_global_args')
0268 if self.extra_vars:
0269 global_args.update(self.extra_vars())
0270 optional_args = {}
0271 if fragment:
0272 optional_args['disable_wrapping'] = True
0273 if output_encoding:
0274 optional_args['output_encoding'] = output_encoding
0275 if encoding_errors:
0276 optional_args['encoding_errors'] = encoding_errors
0277 if disable_unicode:
0278 optional_args['disable_unicode'] = disable_unicode
0279 self.interpreter.execute(template, request_args=info,
0280 global_args=global_args, out_buffer=buf,
0281 **optional_args)
0282 return buf.getvalue()
0283
0284
0285available_engines = {}
0286
0287
0288for entry_point in pkg_resources.iter_entry_points('python.templating.engines'):
0290 try:
0291 Engine = entry_point.load()
0292 available_engines[entry_point.name] = Engine
0293 except:
0294 import sys
0295 from pkg_resources import DistributionNotFound
0296 # Warn when there's a problem loading a Buffet plugin unless it's
0297 # pylonsmyghty reporting there's no Myghty installed
0298 if not isinstance(sys.exc_info()[1], DistributionNotFound) or entry_point.name != 'pylonsmyghty':
0300 import traceback
0301 import warnings
0302 tb = StringIO()
0303 traceback.print_exc(file=tb)
0304 warnings.warn("Unable to load template engine entry point: '%s': "
0305 "%s" % (entry_point, tb.getvalue()), RuntimeWarning,
0306 2)
0307
0308
0309def render(*args, **kargs):
0310 """Render a template and return it as a string (possibly Unicode)
0311
0312 Optionally takes 3 keyword arguments to use caching supplied by Buffet.
0313
0314 Examples:
0315
0316 .. code-block:: Python
0317
0318 content = render('/my/template.mako')
0319 print content
0320 content = render('/my/template2.myt', fragment=True)
0321
0322 .. admonition:: Note
0323
0324 Not all template languages support the concept of a fragment. In those
0325 template languages that do support the fragment option, this usually
0326 implies that the template will be rendered without extending or
0327 inheriting any site skin.
0328 """
0329 fragment = kargs.pop('fragment', False)
0330 format = kargs.pop('format', None)
0331 args = list(args)
0332 template = args.pop()
0333 cache_args = dict(cache_expire=kargs.pop('cache_expire', None),
0334 cache_type=kargs.pop('cache_type', None),
0335 cache_key=kargs.pop('cache_key', None))
0336 log.debug("Render called with %s args and %s keyword args", args, kargs)
0337 if args:
0338 engine = args.pop()
0339 return pylons.buffet.render(engine, template, fragment=fragment,
0340 format=format, namespace=kargs,
0341 **cache_args)
0342 return pylons.buffet.render(template_name=template, fragment=fragment,
0343 format=format, namespace=kargs, **cache_args)
0344
0345
0346def render_response(*args, **kargs):
0347 """Returns the rendered response within a Response object
0348
0349 See ``render`` for information on rendering.
0350
0351 Example:
0352
0353 .. code-block:: Python
0354
0355 def view(self):
0356 return render_response('/my/template.mako')
0357 """
0358 warnings.warn(pylons.legacy.render_response_warning,
0359 PendingDeprecationWarning, 2)
0360
0361 response = pylons.response._current_obj()
0362 response.content = render(*args, **kargs)
0363 output_encoding = kargs.get('output_encoding')
0364 encoding_errors = kargs.get('encoding_errors')
0365 if output_encoding:
0366 response.headers['Content-Type'] = '%s; charset=%s' % (pylons.Response.defaults.get('content_type',
0368 'text/html'), output_encoding)
0369 if encoding_errors:
0370 response.encoding_errors = encoding_errors
0371 return response
0372render_response.__doc__ = 'Pending Deprecation: %s.\n\n%s' % (pylons.legacy.render_response_warning, render_response.__doc__)