0001# -*- coding: utf-8 -*-
0002
0003"""Pythonic, XML Templating
0004
0005Kid is a simple, Python-based template language for generating and
0006transforming XML vocabularies. Kid was spawned as a result of a kinky love
0007triangle between XSLT, TAL, and PHP. We believe many of the best features
0008of these languages live on in Kid with much of the limitations and
0009complexity stamped out (well, eventually :).
0010
0011"""
0012
0013__revision__ = "$Rev: 492 $"
0014__date__ = "$Date: 2007-07-06 21:38:45 -0400 (Fri, 06 Jul 2007) $"
0015
0016from kid import release
0017
0018__version__ = release.version
0019__author__ = release.author
0020__email__ = release.email
0021__copyright__ = release.copyright
0022__license__ = release.license
0023
0024import sys, os
0025
0026assert sys.hexversion >= 0x02030000, "Kid templates need Python 2.3 or later"
0027
0028from kid.util import xml_sniff, QuickTextReader
0029from kid.namespace import Namespace
0030from kid.codewriter import KID_XMLNS, raise_template_error
0031from kid.compiler import KID_EXT
0032from kid.element import Element, SubElement, Comment,       ProcessingInstruction, Fragment
0034from kid.parser import ElementStream, XML, document, _coalesce
0035from kid.filter import transform_filter
0036from kid.serialization import Serializer, PlainSerializer,       XMLSerializer, HTMLSerializer, XHTMLSerializer
0038from kid.format import Format, output_formats
0039import kid.template_util as template_util
0040
0041__all__ = ['KID_XMLNS', 'BaseTemplate', 'Template',
0042           'enable_import', 'import_template', 'load_template',
0043           'Element', 'SubElement', 'XML', 'document', 'Namespace',
0044           'Serializer', 'XMLSerializer', 'HTMLSerializer', 'XHTMLSerializer',
0045           'output_methods', 'Format', 'output_formats',
0046           'filter', 'format', 'namespace', 'serialization', 'util']
0047
0048assume_encoding = sys.getdefaultencoding()
0049
0050
0051def enable_import(ext=None, path=None):
0052    """Enable the kid module loader and import hooks.
0053
0054    This function must be called before importing kid templates if templates
0055    are not pre-compiled.
0056
0057    """
0058    import kid.importer
0059    kid.importer.install(ext, path)
0060
0061
0062def disable_import(path=None):
0063    """Disable the kid module loader and import hooks again."""
0064    import kid.importer
0065    kid.importer.uninstall(path)
0066
0067
0068# Turn on import hooks if KID_IMPORT variables are set
0069if os.environ.get('KID_IMPORT'):
0070    enable_import(os.environ.get('KID_IMPORT_EXT'))
0071if os.environ.get('KID_IMPORT_PATH'):
0072    enable_import(os.environ.get('KID_IMPORT_EXT'),
0073        os.environ['KID_IMPORT_PATH'])
0074
0075
0076def import_template(name, encoding=None):
0077    """Import template by name.
0078
0079    This is identical to calling `enable_import` followed by an import
0080    statement. For example, importing a template named foo using the normal
0081    import mechanism looks like this::
0082
0083        import kid
0084        kid.enable_import()
0085        import foo
0086
0087    This function can be used to achieve the same result as follows::
0088
0089        import kid
0090        foo = kid.import_template('foo')
0091
0092    This is sometimes useful when the name of the template is available only
0093    as a string.
0094    """
0095    enable_import()
0096    mod = __import__(name)
0097    components = name.split('.')
0098    for comp in components[1:]:
0099        mod = getattr(mod, comp)
0100    if encoding:
0101        mod.encoding = encoding
0102    return mod
0103
0104
0105def load_template(file, name='', cache=True, encoding=None, ns={},
0106        entity_map=None, exec_module=None):
0107    """Bypass import machinery and load a template module directly.
0108
0109    This can be used as an alternative to accessing templates using the
0110    native python import mechanisms.
0111
0112    file
0113      Can be a filename, a kid template string, or an open file object.
0114    name
0115      Optionally specifies the module name to use for this template. This
0116      is a hack to enable relative imports in templates.
0117    cache
0118      Whether to look for a byte-compiled version of the template. If
0119      no byte-compiled version is found, an attempt is made to dump a
0120      byte-compiled version after compiling. This argument is ignored if
0121      file is not a filename.
0122    entity_map
0123      Entity map to be used when parsing the template.
0124    exec_module
0125      If you want full control over how the template module is executed,
0126      you can provide this callable that will be called with the template
0127      module and the code to be executed as parameters, after the code has
0128      been compiled and the module has been created.
0129
0130    """
0131    if isinstance(file, basestring):
0132        if xml_sniff(file):
0133            fo = QuickTextReader(file)
0134            filename = '<string>'
0135        else:
0136            fo = None
0137            filename = file
0138    else:
0139        fo = file
0140        filename = '<string>'
0141    import kid.importer as importer
0142    if filename != '<string>':
0143        abs_filename = path.find(filename)
0144        if not abs_filename:
0145            raise template_util.TemplateNotFound(
0146                "%s (in %s)" % (filename, ', '.join(path.paths)))
0147        filename = abs_filename
0148        name = importer.get_template_name(name, filename)
0149        if sys.modules.has_key(name):
0150            return sys.modules.get(name)
0151    import kid.compiler as compiler
0152    if filename == '<string>':
0153        code = compiler.compile(fo, filename, encoding, entity_map)
0154    else:
0155        template = compiler.KidFile(filename, force=False,
0156            encoding=encoding, entity_map=entity_map)
0157        code = template.compile(dump_code=cache,
0158            dump_source=os.environ.get('KID_OUTPUT_PY'))
0159    mod = importer._create_module(code, name, filename,
0160        store=cache, ns=ns, exec_module=exec_module)
0161    return mod
0162
0163
0164# create some default serializers...
0165output_methods = {
0166    'xml': XMLSerializer(decl=True),
0167    'wml': XMLSerializer(decl=True, doctype='wml'),
0168    'xhtml-strict': XHTMLSerializer(decl=False, doctype='xhtml-strict'),
0169    'xhtml': XHTMLSerializer(decl=False, doctype='xhtml'),
0170    'xhtml-frameset': XHTMLSerializer(decl=False, doctype='xhtml-frameset'),
0171    'html-strict': HTMLSerializer(doctype='html-strict'),
0172    'html': HTMLSerializer(doctype='html'),
0173    'html-frameset': HTMLSerializer(doctype='html-frameset'),
0174    'html-quirks': HTMLSerializer(doctype='html-quirks'),
0175    'html-frameset-quirks': HTMLSerializer(doctype='html-frameset-quirks'),
0176    'HTML-strict': HTMLSerializer(doctype='html-strict', transpose=True),
0177    'HTML': HTMLSerializer(doctype='html', transpose=True),
0178    'HTML-frameset': HTMLSerializer(doctype='html-frameset', transpose=True),
0179    'HTML-quirks': HTMLSerializer(doctype='html-quirks', transpose=True),
0180    'HTML-frameset-quirks': HTMLSerializer(doctype='html-frameset-quirks', transpose=True),
0181    'plain': PlainSerializer()}
0182
0183
0184def Template(file=None, source=None, name=None, encoding=None, **kw):
0185    """Get a Template class quickly given a module name, file, or string.
0186
0187    This is a convenience function for getting a template in a variety of
0188    ways. One and only one of the arguments name or file must be specified.
0189
0190    file:string
0191      The template module is loaded by calling
0192      ``load_template(file, name='', cache=True)``
0193    name:string
0194      The kid import hook is enabled and the template module is located
0195      using the normal Python import mechanisms.
0196    source:string
0197      string containing the templates source.
0198
0199    Once the template module is obtained, a new instance of the module's
0200    Template class is created with the keyword arguments passed to this
0201    function.
0202    """
0203    if name:
0204        mod = import_template(name, encoding=encoding)
0205    elif file is not None:
0206        mod = load_template(file, name=name, encoding=encoding)
0207    elif source is not None:
0208        mod = load_template(QuickTextReader(source),
0209            name=name or hex(id(source)), encoding=encoding)
0210    else:
0211        raise template_util.TemplateError(
0212            "Must specify one of name, file, or source.")
0213    try:
0214        mod.Template.module = mod
0215    except Exception:
0216        raise template_util.TemplateImportError(
0217            "Template could not be initialized.")
0218    return mod.Template(**kw)
0219
0220
0221class BaseTemplate(object):
0222
0223    """Base class for compiled Templates.
0224
0225    All kid template modules expose a class named ``Template`` that
0226    extends from this class making the methods defined here available on
0227    all Template subclasses.
0228
0229    This class should not be instantiated directly.
0230    """
0231
0232    # the serializer to use when writing output
0233    serializer = output_methods['xml']
0234
0235    def __init__(self, *args, **kw):
0236        """
0237        Initialize a template with instance attributes specified by
0238        keyword arguments.
0239
0240        Keyword arguments are available to the template using self.var
0241        notation.
0242        """
0243        for k in kw:
0244            # check that reserved keywords such as 'content' are not used
0245            if hasattr(BaseTemplate, k):
0246                raise template_util.TemplateAttrsError(
0247                    "Keyword argument %r is a reserved name." % k)
0248        self.__dict__.update(kw)
0249        self._filters = [transform_filter]
0250        self._layout_classes = []
0251
0252    def write(self, file, encoding=None,
0253            fragment=False, output=None, format=None):
0254        """
0255        Execute template and write output to file.
0256
0257        file:file
0258          A filename or a file like object (must support write()).
0259        encoding:string
0260          The output encoding. Default: utf-8.
0261        fragment:bool
0262          Controls whether prologue information (such as <?xml?>
0263          declaration and DOCTYPE should be written). Set to True
0264          when generating fragments meant to be inserted into
0265          existing XML documents.
0266        output:string,`Serializer`
0267          A string specifying an output method ('xml', 'html',
0268          'xhtml') or a Serializer object.
0269        """
0270        serializer = self._get_serializer(output)
0271        try:
0272            return serializer.write(self, file, encoding, fragment, format)
0273        except Exception:
0274            raise_template_error(module=self.__module__)
0275
0276    def serialize(self, encoding=None,
0277            fragment=False, output=None, format=None):
0278        """
0279        Execute a template and return a single string.
0280
0281        encoding
0282          The output encoding. Default: utf-8.
0283        fragment
0284          Controls whether prologue information (such as <?xml?>
0285          declaration and DOCTYPE should be written). Set to True
0286          when generating fragments meant to be inserted into
0287          existing XML documents.
0288        output
0289          A string specifying an output method ('xml', 'html',
0290          'xhtml') or a Serializer object.
0291
0292        This is a convienence method, roughly equivalent to::
0293
0294          ''.join([x for x in obj.generate(encoding, fragment, output)]
0295
0296        """
0297        serializer = self._get_serializer(output)
0298        try:
0299            return serializer.serialize(self, encoding, fragment, format)
0300        except Exception:
0301            raise_template_error(module=self.__module__)
0302
0303    def generate(self, encoding=None,
0304            fragment=False, output=None, format=None):
0305        """
0306        Execute template and generate serialized output incrementally.
0307
0308        This method returns an iterator that yields an encoded string
0309        for each iteration. The iteration ends when the template is done
0310        executing.
0311
0312        encoding
0313          The output encoding. Default: utf-8.
0314        fragment
0315          Controls whether prologue information (such as <?xml?>
0316          declaration and DOCTYPE should be written). Set to True
0317          when generating fragments meant to be inserted into
0318          existing XML documents.
0319        output
0320          A string specifying an output method ('xml', 'html',
0321          'xhtml') or a Serializer object.
0322        """
0323        serializer = self._get_serializer(output)
0324        try:
0325            return serializer.generate(self, encoding, fragment, format)
0326        except Exception:
0327            raise_template_error(module=self.__module__)
0328
0329    def __iter__(self):
0330        return iter(self.transform())
0331
0332    def __str__(self):
0333        return self.serialize()
0334
0335    def __unicode__(self):
0336        return unicode(self.serialize(encoding='utf-16'), 'utf-16')
0337
0338    def initialize(self):
0339        pass
0340
0341    def pull(self):
0342        """Returns an iterator over the items in this template."""
0343        # create stream and apply filters
0344        self.initialize()
0345        stream = ElementStream(_coalesce(self.content(),
0346            self._get_assume_encoding()))
0347        return stream
0348
0349    def _pull(self):
0350        """Generate events for this template.
0351
0352        Compiled templates implement this method.
0353        """
0354        return []
0355
0356    def content(self):
0357        from inspect import getmro
0358        visited = self._layout_classes
0359        mro = list(getmro(self.__class__))
0360        mro.reverse()
0361        for c in mro:
0362            if c.__dict__.has_key('layout') and c not in visited:
0363                visited.insert(0, c)
0364                return c.__dict__['layout'](self)
0365        return self._pull()
0366
0367    def transform(self, stream=None, filters=[]):
0368        """
0369        Execute the template and apply any match transformations.
0370
0371        If stream is specified, it must be one of the following:
0372
0373        Element
0374          A kid.Element.
0375        ElementStream
0376          An `pull.ElementStream` instance or other iterator that yields
0377          stream events.
0378        string
0379          A file or URL unless the string starts with
0380          '<' in which case it is considered an XML document
0381          and processed as if it had been an Element.
0382
0383        By default, the `pull` method is called to obtain the stream.
0384        """
0385        if stream is None:
0386            stream = self.pull()
0387        elif isinstance(stream, basestring):
0388            if xml_sniff(stream):
0389                stream = XML(stream, fragment=False)
0390            else:
0391                stream = document(stream)
0392        elif hasattr(stream, 'tag'):
0393            stream = ElementStream(stream)
0394        else:
0395            stream = ElementStream.ensure(stream)
0396        for f in filters + self._filters:
0397            stream = f(stream, self)
0398        return stream
0399
0400    def _get_match_templates(self):
0401        # XXX: use inspect instead of accessing __mro__ directly
0402        try:
0403            rslt = self._match_templates_cached
0404        except AttributeError:
0405            rslt = []
0406            mro = self.__class__.__mro__
0407            for C in mro:
0408                try:
0409                    templates = C._match_templates
0410                except AttributeError:
0411                    continue
0412                rslt += templates
0413            self._match_templates_cached = rslt
0414        return rslt
0415
0416    def _get_serializer(self, serializer):
0417        if serializer is None:
0418            return self.serializer
0419        elif isinstance(serializer, basestring):
0420            return output_methods[serializer]
0421        else:
0422            return serializer
0423
0424    def _get_assume_encoding(self):
0425        global assume_encoding
0426
0427        if hasattr(self, "assume_encoding"):
0428            return self.assume_encoding
0429        else:
0430            return assume_encoding
0431
0432    def defined(self, name):
0433        return hasattr(self, name)
0434
0435    def value_of(self, name, default=None):
0436        return getattr(self, name, default)
0437
0438
0439class TemplatePath(object):
0440    """Finding templates on a list of paths."""
0441
0442    def __init__(self, paths=None):
0443        """Initialize with path list."""
0444        if isinstance(paths, basestring):
0445            paths = paths.split(os.pathsep)
0446        elif paths is None:
0447            paths = []
0448        paths.append(os.getcwd())
0449        self.paths = []
0450        for path in paths:
0451            self.append(path)
0452
0453    def _cleanse_path(self, path):
0454        """Normalize path."""
0455        return os.path.abspath(os.path.normpath(os.path.expanduser(path)))
0456
0457    def insert(self, path, pos=0):
0458        """Insert path to list if not already there."""
0459        path = self._cleanse_path(path)
0460        if path not in self.paths:
0461            self.paths.insert(pos, path)
0462
0463    def append(self, path):
0464        """Append path to list if not already there."""
0465        path = self._cleanse_path(path)
0466        if path not in self.paths:
0467            self.paths.append(path)
0468
0469    def remove(self, path):
0470        """Remove path from list."""
0471        path = self._cleanse_path(path)
0472        self.paths = [p for p in self.paths if p != path]
0473
0474    def find(self, path, rel=None):
0475        """Find file relative to path list and rel."""
0476        path = os.path.normpath(path)
0477        if rel:
0478            rel = [os.path.dirname(rel)]
0479        else:
0480            rel = []
0481        for p in self.paths + rel:
0482            p = os.path.join(p, path)
0483            if os.path.exists(p):
0484                return p
0485            if not p.endswith(KID_EXT):
0486                p += KID_EXT
0487                if os.path.exists(p):
0488                    return p
0489
0490
0491path = TemplatePath()