wiki:NonPackagedKidTemplates
Warning: Can't synchronize with repository "(default)" (Unsupported version control system "svn": No module named svn). Look in the Trac log for more information.

Motivation

You want to deploy a site that uses kid templates in a manner that suites more traditional web deployment models. For example, you are working with web developers who are comfortable with html templating, possibly are familiar with kid, but are not python developers. Additionaly you may have very specific requirements with regard to how your site content is hosted, permissions, existing cacheing systems etc. For these reasons it is more convenient to support a site directory to which kid template files (.kid) can be uploaded to. The standard mechanisms for integrating kid templates with turbogears controlers look like this:

class Root(controllers.RootControllers):
    @expose(template='yourpythonapp.templates.welcome')
    def index(self):
        import time
        return dict(now=time.time())

This uses the TurboKid? plugin to load templates from a python package that you must provide to wrap the templates. This example enables you to rewrite the above as:

class Root(controllers.RootControllers):
    
    # bigsitesupport is the name you gave your turbogears application when you set it up (tg-admin quickstart)

    @expose(template='bigsitesupport.tg/kid/welcome')
    def index(self):
        import time
        return dict(now=time.time())

And have welcome.kid loaded from the file system (possibly via custom caching machinery) without the need to bundle the template in a python package. It does this using a technique that is compatible with TurboKid? and the existing TurboGears template engine machinery.

The python import hook

First create an import hook that is compatible with PEP 302  http://www.python.org/dev/peps/pep-0302/. This enables your customisation to be picked up by TruboKid? with out needing to patch its source. This example is derived from the import machinery that exists in kid.importer  http://www.kid-templating.org/trac/browser/trunk/kid/importer.py.

import os, logging
from sys import path_hooks, path_importer_cache
from kid.importer import import_template

log = logging.getLogger('bigsitesupport.tg.kidimport')

class _Importer(object):
    def __init__(self, config, path=None):
        self.config = config
        # assumes you have used the turbogears [/static] directory to configure the location 
        # of your non packaged kid templates. you could quite reasonably introduce a completely new url
        # mapping for this purpose. using the existing [/static] lets you get things working with less work. 
        self.confstatic=config.get('/static')
        self.staticdir=self.confstatic and self.confstatic.get(
                'static_filter.dir', None)
        self.path = path

    def find_module(self, fullname):
        log.info(
            "CONSIDERING: fullname='%s' initpath='%s' ***" % (
                fullname,self.path))
        if not self.staticdir:
            return
        parts=fullname.split('/', 1)
        if len(parts) != 2:
            log.info("REJECTING: not a uri, [%s]" % fullname)
            return
        filename=os.path.join(self.staticdir, parts[1].replace('/',os.sep))
        filename+='.kid'
        if not os.path.exists(filename):
            log.info("REJECTING: file does not exist [%s]" % filename)
            return
        self._filename = filename
        return self

    def load_module(self, fullname):
        # A production quality implementation would integrate with your custom 
        # caching solution in this method. This expositional implementation will
        # ** force ** recompilation of the  template on every access. Which is fine 
        # for development but not for the real world.

        return import_template(fullname, self._filename, force=1)
        

def Importer_factory(config):
    """Factory method to make turbogears config available to the hook"""
    def factory(*args):
        return _Importer(config,*args)
    return factory

_installed=False
def install_import_hook(factory):
    global _installed
    if not _installed:
        path_hooks.append(factory)
        _installed=True
        path_importer_cache.clear()

def remove_import_hook():
    i = 0
    while i < len(path_hooks):
        if isinstance(path_hooks[i], _Importer):
            del path_hooks[i]
        else:
            i += 1
    path_importer_cache.clear()

The custom start script

It is necessary to customise the way you start your turbogears application to enable the import hook. It may be possible to avoid this customisation. However, it's likely that if you want NonPackagedKidTemplates you will also be customising your turbogears startup process. This script shows you where to enable the hook and how to customise top_source_dir to cause [/static] to resolve to an arbitrary file system location.

#!/usr/bin/env python
import os
def run_turbogears():
    import pkg_resources
    pkg_resources.require("TurboGears")
    from configobj import ConfigObj
    import turbogears, cherrypy
    cherrypy.lowercase_api = True
    from bigsitesupport import kidimport
    TOP_LEVEL_DIR='/var/www/bigsite'
    tg_cfgfiles=[ # order is important
        'conf/tg-dev.cfg',
        'conf/tg-log.cfg',
        'conf/tg-app.cfg'
        ]
    package_dir = os.path.abspath(
        pkg_resources.resource_filename('bigsitesupport.tg', ''
            )[:-1].replace('\\','/'))
    configdata = ConfigObj(unrepr=True)
    defaults = turbogears.config.config_defaults()
    defaults.update(
            top_level_dir=TOP_LEVEL_DIR,
            package_dir=package_dir)
    configdata.merge(dict(DEFAULT=defaults))
    for f in tg_cfgfiles:
        cfgmerge=ConfigObj(f, unrepr=True)
        cfgmerge.merge(dict(DEFAULT=defaults))
        configdata.merge(cfgmerge)
    configdict=configdata.dict()
    turbogears.config.configure_loggers(configdict)
    turbogears.config.update(configdict)
    kidimport.install_import_hook(
        kidimport.Importer_factory(configdata))
    # may the force be with you luke
    from bigsitesupport.tg.controllers import Root
    turbogears.start_server(Root())
 
if __name__=='__main__':
    run_turbogears()

The critical trick

The problem not addressed so far is how to prevent TruboKid? from complaining about your non packaged kid template file. And indeed, how to prevent it from blowing up when it encounters 'bigsitesupport.tg/kid/welcome' as a template name. There are two parts to the trick:

  1. To get TurboKid? to actualy use your import hook: Add kid.precompiled=True to the [global] section of your turbogears app config file, tg-app.cfg in this example. If your config file was generated by runing tg-admin quickstart then after this change the first few lines of this file should look like:
[global]
# The settings in this file should not vary depending on the deployment
# environment. dev.cfg and prod.cfg are the locations for
# the different deployment settings. Settings in this file will
# be overridden by settings in those other files.

kid.precompiled=True
# The commented out values below are the defaults
  1. To pass TurboKid?'s initial sanity check on your template name you must create a subpackage under bigsitesupport, hence the use of bigsitesupport.tg throughout this example.

Explanation

This works because when you tell TurboKid? your template files are 'precompiled', TurboKid? relies on pythons standard __import__ machinery to load the template. But to get that far you need to pass the initial sanity check on your template name. Following this example, all your template references in your controller.py start with 'bigsitesupport.tg', so TurboKid? finds the dot expects as a marker denoting which package your templates logicaly belong in. TurboKid? does not do any further processing on the template name because it then sees that templates are flagged as precompiled and invokes __import__.