comparison markup/template.py @ 1:821114ec4f69

Initial import.
author cmlenz
date Sat, 03 Jun 2006 07:16:01 +0000
parents
children 1add946decb8
comparison
equal deleted inserted replaced
0:20f3417d4171 1:821114ec4f69
1 # -*- coding: utf-8 -*-
2 #
3 # Copyright (C) 2006 Christopher Lenz
4 # All rights reserved.
5 #
6 # This software is licensed as described in the file COPYING, which
7 # you should have received as part of this distribution. The terms
8 # are also available at http://trac.edgewall.com/license.html.
9 #
10 # This software consists of voluntary contributions made by many
11 # individuals. For the exact contribution history, see the revision
12 # history and logs, available at http://projects.edgewall.com/trac/.
13
14 """Template engine that is compatible with Kid (http://kid.lesscode.org) to a
15 certain extent.
16
17 Differences include:
18 * No generation of Python code for a template; the template is "interpreted"
19 * No support for <?python ?> processing instructions
20 * Expressions are evaluated in a more flexible manner, meaning you can use e.g.
21 attribute access notation to access items in a dictionary, etc
22 * Use of XInclude and match templates instead of Kid's py:extends/py:layout
23 directives
24 * Real (thread-safe) search path support
25 * No dependency on ElementTree (due to the lack of pos info)
26 * The original pos of parse events is kept throughout the processing
27 pipeline, so that errors can be tracked back to a specific line/column in
28 the template file
29 * py:match directives use (basic) XPath expressions to match against input
30 nodes, making match templates more powerful while keeping the syntax simple
31
32 Todo items:
33 * XPath support needs a real implementation
34 * Improved error reporting
35 * Support for using directives as elements and not just as attributes, reducing
36 the need for wrapper elements with py:strip=""
37 * Support for py:choose/py:when/py:otherwise (similar to XSLT)
38 * Support for list comprehensions and generator expressions in expressions
39
40 Random thoughts:
41 * Is there any need to support py:extends and/or py:layout?
42 * Could we generate byte code from expressions?
43 """
44
45 import compiler
46 from itertools import chain
47 import os
48 import re
49 from StringIO import StringIO
50
51 from markup.core import Attributes, Stream
52 from markup.eval import Expression
53 from markup.filters import EvalFilter, IncludeFilter, MatchFilter, \
54 WhitespaceFilter
55 from markup.input import HTML, XMLParser, XML
56
57 __all__ = ['Context', 'BadDirectiveError', 'TemplateError',
58 'TemplateSyntaxError', 'TemplateNotFound', 'Template',
59 'TemplateLoader']
60
61
62 class TemplateError(Exception):
63 """Base exception class for errors related to template processing."""
64
65
66 class TemplateSyntaxError(TemplateError):
67 """Exception raised when an expression in a template causes a Python syntax
68 error."""
69
70 def __init__(self, message, filename='<string>', lineno=-1, offset=-1):
71 if isinstance(message, SyntaxError) and message.lineno is not None:
72 message = str(message).replace(' (line %d)' % message.lineno, '')
73 TemplateError.__init__(self, message)
74 self.filename = filename
75 self.lineno = lineno
76 self.offset = offset
77
78
79 class BadDirectiveError(TemplateSyntaxError):
80 """Exception raised when an unknown directive is encountered when parsing
81 a template.
82
83 An unknown directive is any attribute using the namespace for directives,
84 with a local name that doesn't match any registered directive.
85 """
86
87 def __init__(self, name, filename='<string>', lineno=-1):
88 TemplateSyntaxError.__init__(self, 'Bad directive "%s"' % name.localname,
89 filename, lineno)
90
91
92 class TemplateNotFound(TemplateError):
93 """Exception raised when a specific template file could not be found."""
94
95 def __init__(self, name, search_path):
96 TemplateError.__init__(self, 'Template "%s" not found' % name)
97 self.search_path = search_path
98
99
100 class Context(object):
101 """A container for template input data.
102
103 A context provides a stack of scopes. Template directives such as loops can
104 push a new scope on the stack with data that should only be available
105 inside the loop. When the loop terminates, that scope can get popped off
106 the stack again.
107
108 >>> ctxt = Context(one='foo', other=1)
109 >>> ctxt.get('one')
110 'foo'
111 >>> ctxt.get('other')
112 1
113 >>> ctxt.push(one='frost')
114 >>> ctxt.get('one')
115 'frost'
116 >>> ctxt.get('other')
117 1
118 >>> ctxt.pop()
119 >>> ctxt.get('one')
120 'foo'
121 """
122
123 def __init__(self, **data):
124 self.stack = [data]
125
126 def __getitem__(self, key):
127 """Get a variable's value, starting at the current context and going
128 upward.
129 """
130 return self.get(key)
131
132 def __repr__(self):
133 return repr(self.stack)
134
135 def __setitem__(self, key, value):
136 """Set a variable in the current context."""
137 self.stack[0][key] = value
138
139 def get(self, key):
140 for frame in self.stack:
141 if key in frame:
142 return frame[key]
143
144 def push(self, **data):
145 self.stack.insert(0, data)
146
147 def pop(self):
148 assert self.stack, 'Pop from empty context stack'
149 self.stack.pop(0)
150
151
152 class Directive(object):
153 """Abstract base class for template directives.
154
155 A directive is basically a callable that takes two parameters: `ctxt` is
156 the template data context, and `stream` is an iterable over the events that
157 the directive applies to.
158
159 Directives can be "anonymous" or "registered". Registered directives can be
160 applied by the template author using an XML attribute with the
161 corresponding name in the template. Such directives should be subclasses of
162 this base class that can be instantiated with two parameters: `template`
163 is the `Template` instance, and `value` is the value of the directive
164 attribute.
165
166 Anonymous directives are simply functions conforming to the protocol
167 described above, and can only be applied programmatically (for example by
168 template filters).
169 """
170 __slots__ = ['expr']
171
172 def __init__(self, template, value, pos):
173 self.expr = value and Expression(value) or None
174
175 def __call__(self, stream, ctxt):
176 raise NotImplementedError
177
178 def __repr__(self):
179 expr = ''
180 if self.expr is not None:
181 expr = ' "%s"' % self.expr.source
182 return '<%s%s>' % (self.__class__.__name__, expr)
183
184
185 class AttrsDirective(Directive):
186 """Implementation of the `py:attrs` template directive.
187
188 The value of the `py:attrs` attribute should be a dictionary. The keys and
189 values of that dictionary will be added as attributes to the element:
190
191 >>> ctxt = Context(foo={'class': 'collapse'})
192 >>> tmpl = Template('''<ul xmlns:py="http://purl.org/kid/ns#">
193 ... <li py:attrs="foo">Bar</li>
194 ... </ul>''')
195 >>> print tmpl.generate(ctxt)
196 <ul>
197 <li class="collapse">Bar</li>
198 </ul>
199
200 If the value evaluates to `None` (or any other non-truth value), no
201 attributes are added:
202
203 >>> ctxt = Context(foo=None)
204 >>> print tmpl.generate(ctxt)
205 <ul>
206 <li>Bar</li>
207 </ul>
208 """
209 def __call__(self, stream, ctxt):
210 kind, (tag, attrib), pos = stream.next()
211 attrs = self.expr.evaluate(ctxt)
212 if attrs:
213 attrib = attrib[:]
214 for name, value in attrs.items():
215 if value is not None:
216 value = unicode(value).strip()
217 attrib.append((name, value))
218 yield kind, (tag, Attributes(attrib)), pos
219 for event in stream:
220 yield event
221
222
223 class ContentDirective(Directive):
224 """Implementation of the `py:content` template directive.
225
226 This directive replaces the content of the element with the result of
227 evaluating the value of the `py:content` attribute:
228
229 >>> ctxt = Context(bar='Bye')
230 >>> tmpl = Template('''<ul xmlns:py="http://purl.org/kid/ns#">
231 ... <li py:content="bar">Hello</li>
232 ... </ul>''')
233 >>> print tmpl.generate(ctxt)
234 <ul>
235 <li>Bye</li>
236 </ul>
237 """
238 def __call__(self, stream, ctxt):
239 kind, data, pos = stream.next()
240 if kind is Stream.START:
241 yield kind, data, pos # emit start tag
242 yield Stream.EXPR, self.expr, pos
243 previous = None
244 try:
245 while True:
246 previous = stream.next()
247 except StopIteration:
248 if previous is not None:
249 yield previous
250
251
252 class DefDirective(Directive):
253 """Implementation of the `py:def` template directive.
254
255 This directive can be used to create "Named Template Functions", which
256 are template snippets that are not actually output during normal
257 processing, but rather can be expanded from expressions in other places
258 in the template.
259
260 A named template function can be used just like a normal Python function
261 from template expressions:
262
263 >>> ctxt = Context(bar='Bye')
264 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
265 ... <p py:def="echo(greeting, name='world')" class="message">
266 ... ${greeting}, ${name}!
267 ... </p>
268 ... ${echo('hi', name='you')}
269 ... </div>''')
270 >>> print tmpl.generate(ctxt)
271 <div>
272 <p class="message">
273 hi, you!
274 </p>
275 </div>
276
277 >>> ctxt = Context(bar='Bye')
278 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
279 ... <p py:def="echo(greeting, name='world')" class="message">
280 ... ${greeting}, ${name}!
281 ... </p>
282 ... <div py:replace="echo('hello')"></div>
283 ... </div>''')
284 >>> print tmpl.generate(ctxt)
285 <div>
286 <p class="message">
287 hello, world!
288 </p>
289 </div>
290 """
291 __slots__ = ['name', 'args', 'defaults', 'stream']
292
293 def __init__(self, template, args, pos):
294 Directive.__init__(self, template, None, pos)
295 ast = compiler.parse(args, 'eval').node
296 self.args = []
297 self.defaults = {}
298 if isinstance(ast, compiler.ast.CallFunc):
299 self.name = ast.node.name
300 for arg in ast.args:
301 if isinstance(arg, compiler.ast.Keyword):
302 self.args.append(arg.name)
303 self.defaults[arg.name] = arg.expr.value
304 else:
305 self.args.append(arg.name)
306 else:
307 self.name = ast.name
308 self.stream = []
309
310 def __call__(self, stream, ctxt):
311 self.stream = list(stream)
312 ctxt[self.name] = lambda *args, **kwargs: self._exec(ctxt, *args,
313 **kwargs)
314 return []
315
316 def _exec(self, ctxt, *args, **kwargs):
317 scope = {}
318 args = list(args) # make mutable
319 for name in self.args:
320 if args:
321 scope[name] = args.pop(0)
322 else:
323 scope[name] = kwargs.pop(name, self.defaults.get(name))
324 ctxt.push(**scope)
325 for event in self.stream:
326 yield event
327 ctxt.pop()
328
329
330 class ForDirective(Directive):
331 """Implementation of the `py:for` template directive.
332
333 >>> ctxt = Context(items=[1, 2, 3])
334 >>> tmpl = Template('''<ul xmlns:py="http://purl.org/kid/ns#">
335 ... <li py:for="item in items">${item}</li>
336 ... </ul>''')
337 >>> print tmpl.generate(ctxt)
338 <ul>
339 <li>1</li><li>2</li><li>3</li>
340 </ul>
341 """
342 __slots__ = ['targets']
343
344 def __init__(self, template, value, pos):
345 targets, expr_source = value.split(' in ', 1)
346 self.targets = [str(name.strip()) for name in targets.split(',')]
347 Directive.__init__(self, template, expr_source, pos)
348
349 def __call__(self, stream, ctxt):
350 iterable = self.expr.evaluate(ctxt, [])
351 if iterable is not None:
352 stream = list(stream)
353 for item in iter(iterable):
354 if len(self.targets) == 1:
355 item = [item]
356 scope = {}
357 for idx, name in enumerate(self.targets):
358 scope[name] = item[idx]
359 ctxt.push(**scope)
360 for event in stream:
361 yield event
362 ctxt.pop()
363
364 def __repr__(self):
365 return '<%s "%s in %s">' % (self.__class__.__name__,
366 ', '.join(self.targets), self.expr.source)
367
368
369 class IfDirective(Directive):
370 """Implementation of the `py:if` template directive.
371
372 >>> ctxt = Context(foo=True, bar='Hello')
373 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
374 ... <b py:if="foo">${bar}</b>
375 ... </div>''')
376 >>> print tmpl.generate(ctxt)
377 <div>
378 <b>Hello</b>
379 </div>
380 """
381 def __call__(self, stream, ctxt):
382 if self.expr.evaluate(ctxt):
383 return stream
384 return []
385
386
387 class MatchDirective(Directive):
388 """Implementation of the `py:match` template directive.
389
390 >>> ctxt = Context()
391 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
392 ... <span py:match="div/greeting">
393 ... Hello ${select('@name')}
394 ... </span>
395 ... <greeting name="Dude" />
396 ... </div>''')
397 >>> print tmpl.generate(ctxt)
398 <div>
399 <span>
400 Hello Dude
401 </span>
402 </div>
403 """
404 __slots__ = ['path', 'stream']
405
406 def __init__(self, template, value, pos):
407 Directive.__init__(self, template, None, pos)
408 template.filters.append(MatchFilter(value, self._handle_match))
409 self.path = value
410 self.stream = []
411
412 def __call__(self, stream, ctxt):
413 self.stream = list(stream)
414 return []
415
416 def __repr__(self):
417 return '<%s "%s">' % (self.__class__.__name__, self.path)
418
419 def _handle_match(self, orig_stream, ctxt):
420 ctxt.push(select=lambda path: Stream(orig_stream).select(path))
421 for event in self.stream:
422 yield event
423 ctxt.pop()
424
425
426 class ReplaceDirective(Directive):
427 """Implementation of the `py:replace` template directive.
428
429 >>> ctxt = Context(bar='Bye')
430 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
431 ... <span py:replace="bar">Hello</span>
432 ... </div>''')
433 >>> print tmpl.generate(ctxt)
434 <div>
435 Bye
436 </div>
437
438 This directive is equivalent to `py:content` combined with `py:strip`,
439 providing a less verbose way to achieve the same effect:
440
441 >>> ctxt = Context(bar='Bye')
442 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
443 ... <span py:content="bar" py:strip="">Hello</span>
444 ... </div>''')
445 >>> print tmpl.generate(ctxt)
446 <div>
447 Bye
448 </div>
449 """
450 def __call__(self, stream, ctxt):
451 kind, data, pos = stream.next()
452 yield Stream.EXPR, self.expr, pos
453
454
455 class StripDirective(Directive):
456 """Implementation of the `py:strip` template directive.
457
458 When the value of the `py:strip` attribute evaluates to `True`, the element
459 is stripped from the output
460
461 >>> ctxt = Context()
462 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
463 ... <div py:strip="True"><b>foo</b></div>
464 ... </div>''')
465 >>> print tmpl.generate(ctxt)
466 <div>
467 <b>foo</b>
468 </div>
469
470 On the other hand, when the attribute evaluates to `False`, the element is
471 not stripped:
472
473 >>> ctxt = Context()
474 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
475 ... <div py:strip="False"><b>foo</b></div>
476 ... </div>''')
477 >>> print tmpl.generate(ctxt)
478 <div>
479 <div><b>foo</b></div>
480 </div>
481
482 Leaving the attribute value empty is equivalent to a truth value:
483
484 >>> ctxt = Context()
485 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
486 ... <div py:strip=""><b>foo</b></div>
487 ... </div>''')
488 >>> print tmpl.generate(ctxt)
489 <div>
490 <b>foo</b>
491 </div>
492
493 This directive is particulary interesting for named template functions or
494 match templates that do not generate a top-level element:
495
496 >>> ctxt = Context()
497 >>> tmpl = Template('''<div xmlns:py="http://purl.org/kid/ns#">
498 ... <div py:def="echo(what)" py:strip="">
499 ... <b>${what}</b>
500 ... </div>
501 ... ${echo('foo')}
502 ... </div>''')
503 >>> print tmpl.generate(ctxt)
504 <div>
505 <b>foo</b>
506 </div>
507 """
508 def __call__(self, stream, ctxt):
509 if self.expr:
510 strip = self.expr.evaluate(ctxt)
511 else:
512 strip = True
513 if strip:
514 stream.next() # skip start tag
515 # can ignore StopIteration since it will just break from this
516 # generator
517 previous = stream.next()
518 for event in stream:
519 yield previous
520 previous = event
521 else:
522 for event in stream:
523 yield event
524
525
526 class Template(object):
527 """Can parse a template and transform it into the corresponding output
528 based on context data.
529 """
530 NAMESPACE = 'http://purl.org/kid/ns#'
531
532 directives = [('def', DefDirective),
533 ('match', MatchDirective),
534 ('for', ForDirective),
535 ('if', IfDirective),
536 ('replace', ReplaceDirective),
537 ('content', ContentDirective),
538 ('attrs', AttrsDirective),
539 ('strip', StripDirective)]
540 _dir_by_name = dict(directives)
541 _dir_order = [directive[1] for directive in directives]
542
543 def __init__(self, source, filename=None):
544 """Initialize a template from either a string or a file-like object."""
545 if isinstance(source, basestring):
546 self.source = StringIO(source)
547 else:
548 self.source = source
549 self.filename = filename or '<string>'
550
551 self.pre_filters = [EvalFilter()]
552 self.filters = []
553 self.post_filters = [WhitespaceFilter()]
554 self.parse()
555
556 def __repr__(self):
557 return '<%s "%s">' % (self.__class__.__name__,
558 os.path.basename(self.filename))
559
560 def parse(self):
561 """Parse the template.
562
563 The parsing stage parses the XML template and constructs a list of
564 directives that will be executed in the render stage. The input is
565 split up into literal output (markup that does not depend on the
566 context data) and actual directives (commands or variable
567 substitution).
568 """
569 stream = [] # list of events of the "compiled" template
570 dirmap = {} # temporary mapping of directives to elements
571 ns_prefix = {}
572 depth = 0
573
574 for kind, data, pos in XMLParser(self.source):
575
576 if kind is Stream.START_NS:
577 # Strip out the namespace declaration for template directives
578 prefix, uri = data
579 if uri == self.NAMESPACE:
580 ns_prefix[prefix] = uri
581 else:
582 stream.append((kind, data, pos))
583
584 elif kind is Stream.END_NS:
585 if data in ns_prefix:
586 del ns_prefix[data]
587 else:
588 stream.append((kind, data, pos))
589
590 elif kind is Stream.START:
591 # Record any directive attributes in start tags
592 tag, attrib = data
593 directives = []
594 new_attrib = []
595 for name, value in attrib:
596 if name.namespace == self.NAMESPACE:
597 cls = self._dir_by_name.get(name.localname)
598 if cls is None:
599 raise BadDirectiveError(name, self.filename, pos[0])
600 else:
601 directives.append(cls(self, value, pos))
602 else:
603 value = list(self._interpolate(value, *pos))
604 new_attrib.append((name, value))
605 if directives:
606 directives.sort(lambda a, b: cmp(self._dir_order.index(a.__class__),
607 self._dir_order.index(b.__class__)))
608 dirmap[(depth, tag)] = (directives, len(stream))
609
610 stream.append((kind, (tag, Attributes(new_attrib)), pos))
611 depth += 1
612
613 elif kind is Stream.END:
614 depth -= 1
615 stream.append((kind, data, pos))
616
617 # If there have have directive attributes with the corresponding
618 # start tag, move the events inbetween into a "subprogram"
619 if (depth, data) in dirmap:
620 directives, start_offset = dirmap.pop((depth, data))
621 substream = stream[start_offset:]
622 stream[start_offset:] = [(Stream.SUB,
623 (directives, substream), pos)]
624
625 elif kind is Stream.TEXT:
626 for kind, data, pos in self._interpolate(data, *pos):
627 stream.append((kind, data, pos))
628
629 else:
630 stream.append((kind, data, pos))
631
632 self.stream = stream
633
634 def generate(self, ctxt):
635 """Transform the template based on the given context data."""
636
637 def _transform(stream):
638 # Apply pre and runtime filters
639 for filter_ in chain(self.pre_filters, self.filters):
640 stream = filter_(iter(stream), ctxt)
641
642 try:
643 for kind, data, pos in stream:
644
645 if kind is Stream.SUB:
646 # This event is a list of directives and a list of
647 # nested events to which those directives should be
648 # applied
649 directives, substream = data
650 directives.reverse()
651 for directive in directives:
652 substream = directive(iter(substream), ctxt)
653 for event in _transform(iter(substream)):
654 yield event
655
656 else:
657 yield kind, data, pos
658 except SyntaxError, err:
659 raise TemplateSyntaxError(err, self.filename, pos[0],
660 pos[1] + (err.offset or 0))
661
662 stream = _transform(self.stream)
663
664 # Apply post-filters
665 for filter_ in self.post_filters:
666 stream = filter_(iter(stream), ctxt)
667
668 return Stream(stream)
669
670 _FULL_EXPR_RE = re.compile(r'(?<!\$)\$\{(.+?)\}')
671 _SHORT_EXPR_RE = re.compile(r'(?<!\$)\$([a-zA-Z][a-zA-Z0-9_\.]*)')
672
673 def _interpolate(cls, text, lineno=-1, offset=-1):
674 """Parse the given string and extract expressions.
675
676 This method returns a list containing both literal text and `Expression`
677 objects.
678
679 @param text: the text to parse
680 @param lineno: the line number at which the text was found (optional)
681 @param offset: the column number at which the text starts in the source
682 (optional)
683 """
684 patterns = [cls._FULL_EXPR_RE, cls._SHORT_EXPR_RE]
685 def _interpolate(text):
686 for idx, group in enumerate(patterns.pop(0).split(text)):
687 if idx % 2:
688 yield Stream.EXPR, Expression(group), (lineno, offset)
689 elif group:
690 if patterns:
691 for result in _interpolate(group):
692 yield result
693 else:
694 yield Stream.TEXT, group.replace('$$', '$'), \
695 (lineno, offset)
696 return _interpolate(text)
697 _interpolate = classmethod(_interpolate)
698
699
700 class TemplateLoader(object):
701 """Responsible for loading templates from files on the specified search
702 path.
703
704 >>> import tempfile
705 >>> fd, path = tempfile.mkstemp(suffix='.html', prefix='template')
706 >>> os.write(fd, '<p>$var</p>')
707 11
708 >>> os.close(fd)
709
710 The template loader accepts a list of directory paths that are then used
711 when searching for template files, in the given order:
712
713 >>> loader = TemplateLoader([os.path.dirname(path)])
714
715 The `load()` method first checks the template cache whether the requested
716 template has already been loaded. If not, it attempts to locate the
717 template file, and returns the corresponding `Template` object:
718
719 >>> template = loader.load(os.path.basename(path))
720 >>> isinstance(template, Template)
721 True
722
723 Template instances are cached: requesting a template with the same name
724 results in the same instance being returned:
725
726 >>> loader.load(os.path.basename(path)) is template
727 True
728 """
729 def __init__(self, search_path=None, auto_reload=False):
730 """Create the template laoder.
731
732 @param search_path: a list of absolute path names that should be
733 searched for template files
734 @param auto_reload: whether to check the last modification time of
735 template files, and reload them if they have changed
736 """
737 self.search_path = search_path
738 if self.search_path is None:
739 self.search_path = []
740 self.auto_reload = auto_reload
741 self._cache = {}
742 self._mtime = {}
743
744 def load(self, filename):
745 """Load the template with the given name.
746
747 This method searches the search path trying to locate a template
748 matching the given name. If no such template is found, a
749 `TemplateNotFound` exception is raised. Otherwise, a `Template` object
750 representing the requested template is returned.
751
752 Template searches are cached to avoid having to parse the same template
753 file more than once. Thus, subsequent calls of this method with the
754 same template file name will return the same `Template` object.
755
756 @param filename: the relative path of the template file to load
757 """
758 filename = os.path.normpath(filename)
759 try:
760 tmpl = self._cache[filename]
761 if not self.auto_reload or \
762 os.path.getmtime(tmpl.filename) == self._mtime[filename]:
763 return tmpl
764 except KeyError:
765 pass
766 for dirname in self.search_path:
767 filepath = os.path.join(dirname, filename)
768 try:
769 fileobj = file(filepath, 'rt')
770 try:
771 tmpl = Template(fileobj, filename=filepath)
772 tmpl.pre_filters.append(IncludeFilter(self))
773 finally:
774 fileobj.close()
775 self._cache[filename] = tmpl
776 self._mtime[filename] = os.path.getmtime(filepath)
777 return tmpl
778 except IOError:
779 continue
780 raise TemplateNotFound(filename, self.search_path)
Copyright (C) 2012-2017 Edgewall Software