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