0001"""Pythonic, XML Templating
0002
0003Kid is a simple, Python-based template language for generating and
0004transforming XML vocabularies. Kid was spawned as a result of a kinky love
0005triangle between XSLT, TAL, and PHP. We believe many of the best features
0006of these languages live on in Kid with much of the limitations and
0007complexity stamped out (well, eventually :).
0008
0009"""
0010
0011__version__ = "0.8"
0012__revision__ = "$Rev: 220 $"
0013__date__ = "$Date: 2005-12-02 02:53:02 -0500 (Fri, 02 Dec 2005) $"
0014__author__ = "Ryan Tomayko (rtomayko@gmail.com)"
0015__copyright__ = "Copyright 2004-2005, Ryan Tomayko"
0016__license__ = "MIT <http://www.opensource.org/licenses/mit-license.php>"
0017
0018import sys
0019import os
0020
0021from kid.util import xml_sniff, QuickTextReader
0022from kid.namespace import Namespace
0023from kid.pull import ElementStream, Element, SubElement, Fragment,                        XML, document, _coalesce
0025from kid.et import ElementTree, Comment, ProcessingInstruction
0026from kid.parser import KID_XMLNS
0027from kid.serialization import Serializer, XMLSerializer, HTMLSerializer, PlainSerializer, XHTMLSerializer
0028
0029assume_encoding = sys.getdefaultencoding()
0030
0031def enable_import(suffixes=None):
0032    """Enable the kid module loader and import hooks.
0033    
0034    This function must be called before importing kid templates if templates
0035    are not pre-compiled.
0036    
0037    Note that if your application uses ZODB, you will need to import ZODB
0038    before calling this function as ZODB's import hooks have some issues if
0039    installed after the kid import hooks.
0040    
0041    """
0042    import kid.importer
0043    kid.importer.install(suffixes)
0044
0045#
0046# Turn on import hook if KID_IMPORT is set
0047#
0048if os.environ.get('KID_IMPORT', None) is not None:
0049    enable_import()
0050
0051def import_template(name):
0052    """Import template by name.
0053
0054    This is identical to calling `enable_import` followed by an import
0055    statement. For example, importing a template named foo using the normal
0056    import mechanism looks like this::
0057
0058        import kid
0059        kid.enable_import()
0060        import foo
0061
0062    This function can be used to achieve the same result as follows::
0063
0064        import kid
0065        foo = kid.import_template('foo')
0066
0067    This is sometimes useful when the name of the template is available only
0068    as a string.
0069    """
0070    enable_import()
0071    mod = __import__(name)
0072    components = name.split('.')
0073    for comp in components[1:]:
0074        mod = getattr(mod, comp)
0075    return mod
0076
0077def load_template(file, name='', cache=1, encoding=None):
0078    """Bypass import machinery and load a template module directly.
0079
0080    This can be used as an alternative to accessing templates using the
0081    native python import mechanisms.
0082    
0083    file
0084      Can be a filename, a kid template string, or an open file object.
0085    name
0086      Optionally specifies the module name to use for this template. This
0087      is a hack to enable relative imports in templates.
0088    cache
0089      Whether to look for a byte-compiled version of the template. If
0090      no byte-compiled version is found, an attempt is made to dump a
0091      byte-compiled version after compiling. This argument is ignored if
0092      file is not a filename.
0093    """
0094    if isinstance(file, (str, unicode)):
0095        if xml_sniff(file):
0096            fo = QuickTextReader(file)
0097            filename = '<string>'
0098        else:
0099            fo = None
0100            filename = file
0101    else:
0102        fo = file
0103        filename = '<string>'
0104    import kid.importer as importer
0105    if filename != '<string>':
0106        abs_filename = path.find(filename)
0107        if not abs_filename:
0108            raise Exception, "Template not found: %s (in %s)" % (
0109                filename, ', '.join(path.paths))
0110        filename = abs_filename
0111        name = importer.get_template_name(name, filename)
0112        if sys.modules.has_key(name):
0113            return sys.modules.get(name)
0114    import kid.compiler as compiler
0115    if filename == '<string>':
0116        code = compiler.compile(fo, filename, encoding)
0117    else:
0118        template = compiler.KidFile(filename, 0, encoding)
0119        code = template.compile(dump_code=cache, dump_source=os.environ.get('KID_OUTPUT_PY'))
0120
0121    mod = importer._create_module(code, name, filename, store=cache)
0122    return mod
0123
0124# create some default serializers..
0125output_methods = {
0126    'xml'          : XMLSerializer(decl=1),
0127    'xhtml'        : XHTMLSerializer(decl=0, doctype='xhtml'),
0128    'xhtml-strict' : XHTMLSerializer(decl=0, doctype='xhtml-strict'),
0129    'html'         : HTMLSerializer(doctype='html'),
0130    'html-strict'  : HTMLSerializer(doctype='html-strict'),
0131    'plain':         PlainSerializer()}
0132
0133def Template(file=None, source=None, name=None, **kw):
0134    """Get a Template class quickly given a module name, file, or string.
0135    
0136    This is a convenience function for getting a template in a variety of
0137    ways. One and only one of the arguments name or file must be specified.
0138    
0139    file:string
0140      The template module is loaded by calling
0141      ``load_template(file, name='', cache=1)``
0142    name:string
0143      The kid import hook is enabled and the template module is located
0144      using the normal Python import mechanisms.
0145    source:string
0146      string containing the templates source.
0147
0148    Once the template module is obtained, a new instance of the module's
0149    Template class is created with the keyword arguments passed to this
0150    function.
0151    """
0152    if name:
0153        mod = import_template(name)
0154    elif file is not None:
0155        mod = load_template(file)
0156    elif source is not None:
0157        mod = load_template(QuickTextReader(source))
0158    else:
0159        raise Exception("Must specify one of name, file, or source.")
0160    mod.Template.module = mod
0161    return mod.Template(**kw)
0162
0163from kid.filter import transform_filter
0164
0165class BaseTemplate(object):
0166
0167    """Base class for compiled Templates.
0168
0169    All kid template modules expose a class named ``Template`` that
0170    extends from this class making the methods defined here available on
0171    all Template subclasses.
0172    
0173    This class should not be instantiated directly.
0174    """
0175
0176    # the serializer to use when writing output
0177    serializer = output_methods['xml']
0178    _filters = [transform_filter]
0179
0180    def __init__(self, *args, **kw):
0181        """
0182        Initialize a template with instance attributes specified by
0183        keyword arguments.
0184           
0185        Keyword arguments are available to the template using self.var
0186        notation.
0187        """
0188        self.__dict__.update(kw)
0189        self._layout_classes = []
0190
0191    def write(self, file, encoding=None, fragment=0, output=None):
0192        """
0193        Execute template and write output to file.
0194        
0195        file:file
0196          A filename or a file like object (must support write()).
0197        encoding:string
0198          The output encoding. Default: utf-8.
0199        fragment:bool
0200          Controls whether prologue information (such as <?xml?>
0201          declaration and DOCTYPE should be written). Set to 1
0202          when generating fragments meant to be inserted into
0203          existing XML documents.
0204        output:string,`Serializer`
0205          A string specifying an output method ('xml', 'html',
0206          'xhtml') or a Serializer object.
0207        """
0208        serializer = self._get_serializer(output)
0209        return serializer.write(self, file, encoding, fragment)
0210
0211    def serialize(self, encoding=None, fragment=0, output=None):
0212        """
0213        Execute a template and return a single string.
0214        
0215        encoding
0216          The output encoding. Default: utf-8.
0217        fragment
0218          Controls whether prologue information (such as <?xml?>
0219          declaration and DOCTYPE should be written). Set to 1
0220          when generating fragments meant to be inserted into
0221          existing XML documents.
0222        output
0223          A string specifying an output method ('xml', 'html',
0224          'xhtml') or a Serializer object.
0225        
0226        This is a convienence method, roughly equivalent to::
0227        
0228          ''.join([x for x in obj.generate(encoding, fragment, output)]
0229        
0230        """
0231        serializer = self._get_serializer(output)
0232        return serializer.serialize(self, encoding, fragment)
0233
0234    def generate(self, encoding=None, fragment=0, output=None):
0235        """
0236        Execute template and generate serialized output incrementally.
0237        
0238        This method returns an iterator that yields an encoded string
0239        for each iteration. The iteration ends when the template is done
0240        executing.
0241        
0242        encoding
0243          The output encoding. Default: utf-8.
0244        fragment
0245          Controls whether prologue information (such as <?xml?>
0246          declaration and DOCTYPE should be written). Set to 1
0247          when generating fragments meant to be inserted into
0248          existing XML documents.
0249        output
0250          A string specifying an output method ('xml', 'html',
0251          'xhtml') or a Serializer object.
0252        """
0253        serializer = self._get_serializer(output)
0254        return serializer.generate(self, encoding, fragment)
0255
0256    def __iter__(self):
0257        return iter(self.transform())
0258
0259    def __str__(self):
0260        return self.serialize()
0261
0262    def __unicode__(self):
0263        return unicode(self.serialize(encoding='utf-16'), 'utf-16')
0264
0265    def initialize(self):
0266        pass
0267
0268    def pull(self):
0269        """Returns an iterator over the items in this template."""
0270        # create stream and apply filters
0271        self.initialize()
0272        stream = ElementStream(_coalesce(self.content(), self._get_assume_encoding()))
0273        return stream
0274
0275    def _pull(self):
0276        """Generate events for this template.
0277        
0278        Compiled templates implement this method.
0279        """
0280        return []
0281
0282    def content(self):
0283        from inspect import getmro
0284        visited = self._layout_classes
0285        mro = list(getmro(self.__class__))
0286        mro.reverse()
0287        for c in mro:
0288            if c.__dict__.has_key('layout') and c not in visited:
0289                visited.insert(0, c)
0290                return c.__dict__['layout'](self)
0291        return self._pull()
0292
0293    def transform(self, stream=None, filters=[]):
0294        """
0295        Execute the template and apply any match transformations.
0296        
0297        If stream is specified, it must be one of the following:
0298        
0299        Element
0300          An ElementTree Element.
0301        ElementStream
0302          An `pull.ElementStream` instance or other iterator that yields
0303          stream events.
0304        string
0305          A file or URL unless the string starts with
0306          '<' in which case it is considered an XML document
0307          and processed as if it had been an Element.
0308
0309        By default, the `pull` method is called to obtain the stream.
0310        """
0311        if stream is None:
0312            stream = self.pull()
0313        elif isinstance(stream, (str, unicode)):
0314            if xml_sniff(stream):
0315                stream = XML(stream, fragment=0)
0316            else:
0317                stream = document(stream)
0318        elif hasattr(stream, 'tag'):
0319            stream = ElementStream(stream)
0320        else:
0321            stream = ElementStream.ensure(stream)
0322        for f in filters + self._filters:
0323            stream = f(stream, self)
0324        return stream
0325
0326    def _get_match_templates(self):
0327        # XXX: use inspect instead of accessing __mro__ directly
0328        try:
0329            rslt = self._match_templates_cached
0330        except AttributeError:
0331            rslt = []
0332            mro = self.__class__.__mro__
0333            for C in mro:
0334                try:
0335                    templates = C._match_templates
0336                except AttributeError:
0337                    continue
0338                rslt += templates
0339            self._match_templates_cached = rslt
0340        return rslt
0341
0342    def _get_serializer(self, serializer):
0343        if serializer is None:
0344            return self.serializer
0345        elif isinstance(serializer, (str, unicode)):
0346            return output_methods[serializer]
0347        else:
0348            return serializer
0349
0350    def _get_assume_encoding(self):
0351        global assume_encoding
0352
0353        if hasattr(self, "assume_encoding"):
0354            return self.assume_encoding
0355        else:
0356            return assume_encoding
0357
0358class TemplatePath(object):
0359    def __init__(self, paths=None):
0360        from os.path import normpath, expanduser, abspath
0361        if isinstance(paths, (str, unicode)):
0362            paths = [paths]
0363        elif paths is None:
0364            paths = []
0365        paths += [os.getcwd(), '/']
0366        self.paths = [abspath(normpath(expanduser(p))) for p in paths]
0367        self.cache = {}
0368
0369    def relativize(self, file, path):
0370        from os.path import normpath, join, dirname, abspath, split, sep
0371        head, tail = (dirname(abspath(file)), '')
0372        parts = path.split(sep)
0373        paths = self.paths
0374        while 1:
0375            if head in paths or head == '/':
0376                return join(*parts)
0377            head, tail = split(head)
0378            parts.insert(0, tail)
0379
0380    def insert(self, path, pos=0):
0381        from os.path import normpath, expanduser, abspath
0382        self.paths.insert(pos, abspath(normpath(expanduser(path))))
0383        self.cache = {}
0384
0385    def append(self, path):
0386        self.insert(path, len(self.paths))
0387
0388    def find(self, path, rel=None):
0389        cache = self.cache
0390        from os.path import normpath, join, exists
0391        path = normpath(path)
0392        if rel:
0393            path = self.relativize(rel, path)
0394        if path in cache:
0395            return cache[path]
0396        for p in self.paths:
0397            p = join(p, path)
0398            if exists(p):
0399                return p
0400
0401path = TemplatePath()
0402
0403__all__ = ['KID_XMLNS', 'BaseTemplate', 'Template',
0404           'enable_import', 'import_template', 'load_template',
0405           'Element', 'SubElement', 'XML', 'document', 'Namespace',
0406           'Serializer', 'XMLSerializer', 'HTMLSerializer', 'XHTMLSerializer', 'output_methods',
0407           'filter', 'namespace', 'serialization', 'util']