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
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
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
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
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
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']