comparison genshi/template/base.py @ 820:1837f39efd6f experimental-inline

Sync (old) experimental inline branch with trunk@1027.
author cmlenz
date Wed, 11 Mar 2009 17:51:06 +0000
parents 0742f421caba
children eb8aa8690480
comparison
equal deleted inserted replaced
500:0742f421caba 820:1837f39efd6f
1 # -*- coding: utf-8 -*- 1 # -*- coding: utf-8 -*-
2 # 2 #
3 # Copyright (C) 2006-2007 Edgewall Software 3 # Copyright (C) 2006-2008 Edgewall Software
4 # All rights reserved. 4 # All rights reserved.
5 # 5 #
6 # This software is licensed as described in the file COPYING, which 6 # This software is licensed as described in the file COPYING, which
7 # you should have received as part of this distribution. The terms 7 # you should have received as part of this distribution. The terms
8 # are also available at http://genshi.edgewall.org/wiki/License. 8 # are also available at http://genshi.edgewall.org/wiki/License.
19 class deque(list): 19 class deque(list):
20 def appendleft(self, x): self.insert(0, x) 20 def appendleft(self, x): self.insert(0, x)
21 def popleft(self): return self.pop(0) 21 def popleft(self): return self.pop(0)
22 import os 22 import os
23 from StringIO import StringIO 23 from StringIO import StringIO
24 import sys
24 25
25 from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure 26 from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
26 from genshi.input import ParseError 27 from genshi.input import ParseError
27 28
28 __all__ = ['Context', 'Template', 'TemplateError', 'TemplateRuntimeError', 29 __all__ = ['Context', 'DirectiveFactory', 'Template', 'TemplateError',
29 'TemplateSyntaxError', 'BadDirectiveError'] 30 'TemplateRuntimeError', 'TemplateSyntaxError', 'BadDirectiveError']
30 __docformat__ = 'restructuredtext en' 31 __docformat__ = 'restructuredtext en'
31 32
32 33
33 class TemplateError(Exception): 34 class TemplateError(Exception):
34 """Base exception class for errors related to template processing.""" 35 """Base exception class for errors related to template processing."""
35 36
36 def __init__(self, message, filename='<string>', lineno=-1, offset=-1): 37 def __init__(self, message, filename=None, lineno=-1, offset=-1):
37 """Create the exception. 38 """Create the exception.
38 39
39 :param message: the error message 40 :param message: the error message
40 :param filename: the filename of the template 41 :param filename: the filename of the template
41 :param lineno: the number of line in the template at which the error 42 :param lineno: the number of line in the template at which the error
42 occurred 43 occurred
43 :param offset: the column number at which the error occurred 44 :param offset: the column number at which the error occurred
44 """ 45 """
46 if filename is None:
47 filename = '<string>'
45 self.msg = message #: the error message string 48 self.msg = message #: the error message string
46 if filename != '<string>' or lineno >= 0: 49 if filename != '<string>' or lineno >= 0:
47 message = '%s (%s, line %d)' % (self.msg, filename, lineno) 50 message = '%s (%s, line %d)' % (self.msg, filename, lineno)
48 Exception.__init__(self, message) 51 Exception.__init__(self, message)
49 self.filename = filename #: the name of the template file 52 self.filename = filename #: the name of the template file
54 class TemplateSyntaxError(TemplateError): 57 class TemplateSyntaxError(TemplateError):
55 """Exception raised when an expression in a template causes a Python syntax 58 """Exception raised when an expression in a template causes a Python syntax
56 error, or the template is not well-formed. 59 error, or the template is not well-formed.
57 """ 60 """
58 61
59 def __init__(self, message, filename='<string>', lineno=-1, offset=-1): 62 def __init__(self, message, filename=None, lineno=-1, offset=-1):
60 """Create the exception 63 """Create the exception
61 64
62 :param message: the error message 65 :param message: the error message
63 :param filename: the filename of the template 66 :param filename: the filename of the template
64 :param lineno: the number of line in the template at which the error 67 :param lineno: the number of line in the template at which the error
76 79
77 An unknown directive is any attribute using the namespace for directives, 80 An unknown directive is any attribute using the namespace for directives,
78 with a local name that doesn't match any registered directive. 81 with a local name that doesn't match any registered directive.
79 """ 82 """
80 83
81 def __init__(self, name, filename='<string>', lineno=-1): 84 def __init__(self, name, filename=None, lineno=-1):
82 """Create the exception 85 """Create the exception
83 86
84 :param name: the name of the directive 87 :param name: the name of the directive
85 :param filename: the filename of the template 88 :param filename: the filename of the template
86 :param lineno: the number of line in the template at which the error 89 :param lineno: the number of line in the template at which the error
127 """ 130 """
128 self.frames = deque([data]) 131 self.frames = deque([data])
129 self.pop = self.frames.popleft 132 self.pop = self.frames.popleft
130 self.push = self.frames.appendleft 133 self.push = self.frames.appendleft
131 self._match_templates = [] 134 self._match_templates = []
135 self._choice_stack = []
132 136
133 # Helper functions for use in expressions 137 # Helper functions for use in expressions
134 def defined(name): 138 def defined(name):
135 """Return whether a variable with the specified name exists in the 139 """Return whether a variable with the specified name exists in the
136 expression scope.""" 140 expression scope."""
149 """Return whether a variable exists in any of the scopes. 153 """Return whether a variable exists in any of the scopes.
150 154
151 :param key: the name of the variable 155 :param key: the name of the variable
152 """ 156 """
153 return self._find(key)[1] is not None 157 return self._find(key)[1] is not None
158 has_key = __contains__
154 159
155 def __delitem__(self, key): 160 def __delitem__(self, key):
156 """Remove a variable from all scopes. 161 """Remove a variable from all scopes.
157 162
158 :param key: the name of the variable 163 :param key: the name of the variable
232 237
233 :return: a list of variables 238 :return: a list of variables
234 """ 239 """
235 return [(key, self.get(key)) for key in self.keys()] 240 return [(key, self.get(key)) for key in self.keys()]
236 241
242 def update(self, mapping):
243 """Update the context from the mapping provided."""
244 self.frames[0].update(mapping)
245
237 def push(self, data): 246 def push(self, data):
238 """Push a new scope on the stack. 247 """Push a new scope on the stack.
239 248
240 :param data: the data dictionary to push on the context stack. 249 :param data: the data dictionary to push on the context stack.
241 """ 250 """
242 251
243 def pop(self): 252 def pop(self):
244 """Pop the top-most scope from the stack.""" 253 """Pop the top-most scope from the stack."""
245 254
246 255
247 def _apply_directives(stream, ctxt, directives): 256 def _apply_directives(stream, directives, ctxt, **vars):
248 """Apply the given directives to the stream. 257 """Apply the given directives to the stream.
249 258
250 :param stream: the stream the directives should be applied to 259 :param stream: the stream the directives should be applied to
260 :param directives: the list of directives to apply
251 :param ctxt: the `Context` 261 :param ctxt: the `Context`
252 :param directives: the list of directives to apply 262 :param vars: additional variables that should be available when Python
263 code is executed
253 :return: the stream with the given directives applied 264 :return: the stream with the given directives applied
254 """ 265 """
255 if directives: 266 if directives:
256 stream = directives[0](iter(stream), ctxt, directives[1:]) 267 stream = directives[0](iter(stream), directives[1:], ctxt, **vars)
257 return stream 268 return stream
258 269
259 270 def _eval_expr(expr, ctxt, **vars):
260 class TemplateMeta(type): 271 """Evaluate the given `Expression` object.
261 """Meta class for templates.""" 272
273 :param expr: the expression to evaluate
274 :param ctxt: the `Context`
275 :param vars: additional variables that should be available to the
276 expression
277 :return: the result of the evaluation
278 """
279 if vars:
280 ctxt.push(vars)
281 retval = expr.evaluate(ctxt)
282 if vars:
283 ctxt.pop()
284 return retval
285
286 def _exec_suite(suite, ctxt, **vars):
287 """Execute the given `Suite` object.
288
289 :param suite: the code suite to execute
290 :param ctxt: the `Context`
291 :param vars: additional variables that should be available to the
292 code
293 """
294 if vars:
295 ctxt.push(vars)
296 ctxt.push({})
297 suite.execute(ctxt)
298 if vars:
299 top = ctxt.pop()
300 ctxt.pop()
301 ctxt.frames[0].update(top)
302
303
304 class DirectiveFactoryMeta(type):
305 """Meta class for directive factories."""
262 306
263 def __new__(cls, name, bases, d): 307 def __new__(cls, name, bases, d):
264 if 'directives' in d: 308 if 'directives' in d:
265 d['_dir_by_name'] = dict(d['directives']) 309 d['_dir_by_name'] = dict(d['directives'])
266 d['_dir_order'] = [directive[1] for directive in d['directives']] 310 d['_dir_order'] = [directive[1] for directive in d['directives']]
267 311
268 return type.__new__(cls, name, bases, d) 312 return type.__new__(cls, name, bases, d)
269 313
270 314
271 class Template(object): 315 class DirectiveFactory(object):
316 """Base for classes that provide a set of template directives.
317
318 :since: version 0.6
319 """
320 __metaclass__ = DirectiveFactoryMeta
321
322 directives = []
323 """A list of `(name, cls)` tuples that define the set of directives
324 provided by this factory.
325 """
326
327 def compare_directives(self):
328 """Return a function that takes two directive classes and compares
329 them to determine their relative ordering.
330 """
331 def _get_index(cls):
332 if cls in self._dir_order:
333 return self._dir_order.index(cls)
334 return 0
335 return lambda a, b: cmp(_get_index(a[0]), _get_index(b[0]))
336
337 def get_directive(self, name):
338 """Return the directive class for the given name.
339
340 :param name: the directive name as used in the template
341 :return: the directive class
342 :see: `Directive`
343 """
344 return self._dir_by_name.get(name)
345
346
347 class Template(DirectiveFactory):
272 """Abstract template base class. 348 """Abstract template base class.
273 349
274 This class implements most of the template processing model, but does not 350 This class implements most of the template processing model, but does not
275 specify the syntax of templates. 351 specify the syntax of templates.
276 """ 352 """
277 __metaclass__ = TemplateMeta 353
354 EXEC = StreamEventKind('EXEC')
355 """Stream event kind representing a Python code suite to execute."""
278 356
279 EXPR = StreamEventKind('EXPR') 357 EXPR = StreamEventKind('EXPR')
280 """Stream event kind representing a Python expression.""" 358 """Stream event kind representing a Python expression."""
281 359
282 INCLUDE = StreamEventKind('INCLUDE') 360 INCLUDE = StreamEventKind('INCLUDE')
285 SUB = StreamEventKind('SUB') 363 SUB = StreamEventKind('SUB')
286 """Stream event kind representing a nested stream to which one or more 364 """Stream event kind representing a nested stream to which one or more
287 directives should be applied. 365 directives should be applied.
288 """ 366 """
289 367
290 def __init__(self, source, basedir=None, filename=None, loader=None, 368 serializer = None
291 encoding=None, lookup='lenient'): 369 _number_conv = unicode # function used to convert numbers to event data
370
371 def __init__(self, source, filepath=None, filename=None, loader=None,
372 encoding=None, lookup='strict', allow_exec=True):
292 """Initialize a template from either a string, a file-like object, or 373 """Initialize a template from either a string, a file-like object, or
293 an already parsed markup stream. 374 an already parsed markup stream.
294 375
295 :param source: a string, file-like object, or markup stream to read the 376 :param source: a string, file-like object, or markup stream to read the
296 template from 377 template from
297 :param basedir: the base directory containing the template file; when 378 :param filepath: the absolute path to the template file
298 loaded from a `TemplateLoader`, this will be the 379 :param filename: the path to the template file relative to the search
299 directory on the template search path in which the 380 path
300 template was found
301 :param filename: the name of the template file, relative to the given
302 base directory
303 :param loader: the `TemplateLoader` to use for loading included 381 :param loader: the `TemplateLoader` to use for loading included
304 templates 382 templates
305 :param encoding: the encoding of the `source` 383 :param encoding: the encoding of the `source`
306 :param lookup: the variable lookup mechanism; either "lenient" (the 384 :param lookup: the variable lookup mechanism; either "strict" (the
307 default), "strict", or a custom lookup class 385 default), "lenient", or a custom lookup class
308 """ 386 :param allow_exec: whether Python code blocks in templates should be
309 self.basedir = basedir 387 allowed
388
389 :note: Changed in 0.5: Added the `allow_exec` argument
390 """
391 self.filepath = filepath or filename
310 self.filename = filename 392 self.filename = filename
311 if basedir and filename:
312 self.filepath = os.path.join(basedir, filename)
313 else:
314 self.filepath = filename
315 self.loader = loader 393 self.loader = loader
316 self.lookup = lookup 394 self.lookup = lookup
395 self.allow_exec = allow_exec
396 self._init_filters()
397 self._prepared = False
317 398
318 if isinstance(source, basestring): 399 if isinstance(source, basestring):
319 source = StringIO(source) 400 source = StringIO(source)
320 else: 401 else:
321 source = source 402 source = source
322 try: 403 try:
323 self.stream = list(self._prepare(self._parse(source, encoding))) 404 self._stream = self._parse(source, encoding)
324 except ParseError, e: 405 except ParseError, e:
325 raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset) 406 raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset)
326 self.filters = [self._flatten, self._eval] 407
327 if loader: 408 def __getstate__(self):
328 self.filters.append(self._include) 409 state = self.__dict__.copy()
410 state['filters'] = []
411 return state
412
413 def __setstate__(self, state):
414 self.__dict__ = state
415 self._init_filters()
329 416
330 def __repr__(self): 417 def __repr__(self):
331 return '<%s "%s">' % (self.__class__.__name__, self.filename) 418 return '<%s "%s">' % (self.__class__.__name__, self.filename)
419
420 def _init_filters(self):
421 self.filters = [self._flatten]
422 if self.loader:
423 self.filters.append(self._include)
424
425 def _get_stream(self):
426 if not self._prepared:
427 self._stream = list(self._prepare(self._stream))
428 self._prepared = True
429 return self._stream
430 stream = property(_get_stream)
332 431
333 def _parse(self, source, encoding): 432 def _parse(self, source, encoding):
334 """Parse the template. 433 """Parse the template.
335 434
336 The parsing stage parses the template and constructs a list of 435 The parsing stage parses the template and constructs a list of
347 def _prepare(self, stream): 446 def _prepare(self, stream):
348 """Call the `attach` method of every directive found in the template. 447 """Call the `attach` method of every directive found in the template.
349 448
350 :param stream: the event stream of the template 449 :param stream: the event stream of the template
351 """ 450 """
451 from genshi.template.loader import TemplateNotFound
452
352 for kind, data, pos in stream: 453 for kind, data, pos in stream:
353 if kind is SUB: 454 if kind is SUB:
354 directives = [] 455 directives = []
355 substream = data[1] 456 substream = data[1]
356 for cls, value, namespaces, pos in data[0]: 457 for cls, value, namespaces, pos in data[0]:
364 else: 465 else:
365 for event in substream: 466 for event in substream:
366 yield event 467 yield event
367 else: 468 else:
368 if kind is INCLUDE: 469 if kind is INCLUDE:
369 data = data[0], list(self._prepare(data[1])) 470 href, cls, fallback = data
471 if isinstance(href, basestring) and \
472 not getattr(self.loader, 'auto_reload', True):
473 # If the path to the included template is static, and
474 # auto-reloading is disabled on the template loader,
475 # the template is inlined into the stream
476 try:
477 tmpl = self.loader.load(href, relative_to=pos[0],
478 cls=cls or self.__class__)
479 for event in tmpl.stream:
480 yield event
481 except TemplateNotFound:
482 if fallback is None:
483 raise
484 for event in self._prepare(fallback):
485 yield event
486 continue
487 elif fallback:
488 # Otherwise the include is performed at run time
489 data = href, cls, list(self._prepare(fallback))
490
370 yield kind, data, pos 491 yield kind, data, pos
492
493 def compile(self):
494 """Compile the template to a Python module, and return the module
495 object.
496 """
497 from imp import new_module
498 from genshi.template.inline import inline
499
500 name = (self.filename or '_some_ident').replace('.', '_')
501 module = new_module(name)
502 source = u'\n'.join(list(inline(self)))
503 code = compile(source, self.filepath or '<string>', 'exec')
504 exec code in module.__dict__, module.__dict__
505 return module
371 506
372 def generate(self, *args, **kwargs): 507 def generate(self, *args, **kwargs):
373 """Apply the template to the given context data. 508 """Apply the template to the given context data.
374 509
375 Any keyword arguments are made available to the template as context 510 Any keyword arguments are made available to the template as context
380 This calling style is used for internal processing. 515 This calling style is used for internal processing.
381 516
382 :return: a markup event stream representing the result of applying 517 :return: a markup event stream representing the result of applying
383 the template to the context data. 518 the template to the context data.
384 """ 519 """
520 vars = {}
385 if args: 521 if args:
386 assert len(args) == 1 522 assert len(args) == 1
387 ctxt = args[0] 523 ctxt = args[0]
388 if ctxt is None: 524 if ctxt is None:
389 ctxt = Context(**kwargs) 525 ctxt = Context(**kwargs)
526 else:
527 vars = kwargs
390 assert isinstance(ctxt, Context) 528 assert isinstance(ctxt, Context)
391 else: 529 else:
392 ctxt = Context(**kwargs) 530 ctxt = Context(**kwargs)
393 531
394 stream = self.stream 532 stream = self.stream
395 for filter_ in self.filters: 533 for filter_ in self.filters:
396 stream = filter_(iter(stream), ctxt) 534 stream = filter_(iter(stream), ctxt, **vars)
397 return Stream(stream) 535 return Stream(stream, self.serializer)
398 536
399 def _eval(self, stream, ctxt): 537 def _flatten(self, stream, ctxt, **vars):
400 """Internal stream filter that evaluates any expressions in `START` and 538 number_conv = self._number_conv
401 `TEXT` events.
402 """
403 filters = (self._flatten, self._eval)
404 539
405 for kind, data, pos in stream: 540 for kind, data, pos in stream:
406 541
407 if kind is START and data[1]: 542 if kind is START and data[1]:
408 # Attributes may still contain expressions in start tags at 543 # Attributes may still contain expressions in start tags at
409 # this point, so do some evaluation 544 # this point, so do some evaluation
410 tag, attrs = data 545 tag, attrs = data
411 new_attrs = [] 546 new_attrs = []
412 for name, substream in attrs: 547 for name, substream in attrs:
413 if isinstance(substream, basestring): 548 if type(substream) is list:
414 value = substream
415 else:
416 values = [] 549 values = []
417 for subkind, subdata, subpos in self._eval(substream, 550 for event in self._flatten(substream, ctxt, **vars):
418 ctxt): 551 if event[0] is TEXT:
419 if subkind is TEXT: 552 values.append(event[1])
420 values.append(subdata)
421 value = [x for x in values if x is not None] 553 value = [x for x in values if x is not None]
422 if not value: 554 if not value:
423 continue 555 continue
556 else:
557 value = substream
424 new_attrs.append((name, u''.join(value))) 558 new_attrs.append((name, u''.join(value)))
425 yield kind, (tag, Attrs(new_attrs)), pos 559 yield kind, (tag, Attrs(new_attrs)), pos
426 560
427 elif kind is EXPR: 561 elif kind is EXPR:
428 result = data.evaluate(ctxt) 562 result = _eval_expr(data, ctxt, **vars)
429 if result is not None: 563 if result is not None:
430 # First check for a string, otherwise the iterable test below 564 # First check for a string, otherwise the iterable test
431 # succeeds, and the string will be chopped up into individual 565 # below succeeds, and the string will be chopped up into
432 # characters 566 # individual characters
433 if isinstance(result, basestring): 567 if isinstance(result, basestring):
434 yield TEXT, result, pos 568 yield TEXT, result, pos
569 elif isinstance(result, (int, float, long)):
570 yield TEXT, number_conv(result), pos
435 elif hasattr(result, '__iter__'): 571 elif hasattr(result, '__iter__'):
436 substream = _ensure(result) 572 for event in self._flatten(_ensure(result), ctxt,
437 for filter_ in filters: 573 **vars):
438 substream = filter_(substream, ctxt)
439 for event in substream:
440 yield event 574 yield event
441 else: 575 else:
442 yield TEXT, unicode(result), pos 576 yield TEXT, unicode(result), pos
443 577
578 elif kind is EXEC:
579 _exec_suite(data, ctxt, **vars)
580
581 elif kind is SUB:
582 # This event is a list of directives and a list of nested
583 # events to which those directives should be applied
584 substream = _apply_directives(data[1], data[0], ctxt, **vars)
585 for event in self._flatten(substream, ctxt, **vars):
586 yield event
587
444 else: 588 else:
445 yield kind, data, pos 589 yield kind, data, pos
446 590
447 def _flatten(self, stream, ctxt): 591 def _include(self, stream, ctxt, **vars):
448 """Internal stream filter that expands `SUB` events in the stream."""
449 for event in stream:
450 if event[0] is SUB:
451 # This event is a list of directives and a list of nested
452 # events to which those directives should be applied
453 directives, substream = event[1]
454 substream = _apply_directives(substream, ctxt, directives)
455 for event in self._flatten(substream, ctxt):
456 yield event
457 else:
458 yield event
459
460 def _include(self, stream, ctxt):
461 """Internal stream filter that performs inclusion of external 592 """Internal stream filter that performs inclusion of external
462 template files. 593 template files.
463 """ 594 """
464 from genshi.template.loader import TemplateNotFound 595 from genshi.template.loader import TemplateNotFound
465 596
466 for event in stream: 597 for event in stream:
467 if event[0] is INCLUDE: 598 if event[0] is INCLUDE:
468 href, fallback = event[1] 599 href, cls, fallback = event[1]
469 if not isinstance(href, basestring): 600 if not isinstance(href, basestring):
470 parts = [] 601 parts = []
471 for subkind, subdata, subpos in self._eval(href, ctxt): 602 for subkind, subdata, subpos in self._flatten(href, ctxt,
603 **vars):
472 if subkind is TEXT: 604 if subkind is TEXT:
473 parts.append(subdata) 605 parts.append(subdata)
474 href = u''.join([x for x in parts if x is not None]) 606 href = u''.join([x for x in parts if x is not None])
475 try: 607 try:
476 tmpl = self.loader.load(href, relative_to=event[2][0], 608 tmpl = self.loader.load(href, relative_to=event[2][0],
477 cls=self.__class__) 609 cls=cls or self.__class__)
478 for event in tmpl.generate(ctxt): 610 for event in tmpl.generate(ctxt, **vars):
479 yield event 611 yield event
480 except TemplateNotFound: 612 except TemplateNotFound:
481 if fallback is None: 613 if fallback is None:
482 raise 614 raise
483 for filter_ in self.filters: 615 for filter_ in self.filters:
484 fallback = filter_(iter(fallback), ctxt) 616 fallback = filter_(iter(fallback), ctxt, **vars)
485 for event in fallback: 617 for event in fallback:
486 yield event 618 yield event
487 else: 619 else:
488 yield event 620 yield event
489 621
490 622
623 EXEC = Template.EXEC
491 EXPR = Template.EXPR 624 EXPR = Template.EXPR
492 INCLUDE = Template.INCLUDE 625 INCLUDE = Template.INCLUDE
493 SUB = Template.SUB 626 SUB = Template.SUB
Copyright (C) 2012-2017 Edgewall Software