Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/genshi/template/base.py: 25%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2006-2010 Edgewall Software
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://genshi.edgewall.org/wiki/License.
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://genshi.edgewall.org/log/.
14"""Basic templating functionality."""
16from collections import deque
17import os
19from genshi.compat import add_metaclass, numeric_types, string_types, text_type, StringIO, BytesIO
20from genshi.core import Attrs, Stream, StreamEventKind, START, TEXT, _ensure
21from genshi.input import ParseError
23__all__ = ['Context', 'DirectiveFactory', 'Template', 'TemplateError',
24 'TemplateRuntimeError', 'TemplateSyntaxError', 'BadDirectiveError']
25__docformat__ = 'restructuredtext en'
28class TemplateError(Exception):
29 """Base exception class for errors related to template processing."""
31 def __init__(self, message, filename=None, lineno=-1, offset=-1):
32 """Create the exception.
34 :param message: the error message
35 :param filename: the filename of the template
36 :param lineno: the number of line in the template at which the error
37 occurred
38 :param offset: the column number at which the error occurred
39 """
40 if filename is None:
41 filename = '<string>'
42 self.msg = message #: the error message string
43 if filename != '<string>' or lineno >= 0:
44 message = '%s (%s, line %d)' % (self.msg, filename, lineno)
45 Exception.__init__(self, message)
46 self.filename = filename #: the name of the template file
47 self.lineno = lineno #: the number of the line containing the error
48 self.offset = offset #: the offset on the line
51class TemplateSyntaxError(TemplateError):
52 """Exception raised when an expression in a template causes a Python syntax
53 error, or the template is not well-formed.
54 """
56 def __init__(self, message, filename=None, lineno=-1, offset=-1):
57 """Create the exception
59 :param message: the error message
60 :param filename: the filename of the template
61 :param lineno: the number of line in the template at which the error
62 occurred
63 :param offset: the column number at which the error occurred
64 """
65 if isinstance(message, SyntaxError) and message.lineno is not None:
66 message = str(message).replace(' (line %d)' % message.lineno, '')
67 TemplateError.__init__(self, message, filename, lineno)
70class BadDirectiveError(TemplateSyntaxError):
71 """Exception raised when an unknown directive is encountered when parsing
72 a template.
74 An unknown directive is any attribute using the namespace for directives,
75 with a local name that doesn't match any registered directive.
76 """
78 def __init__(self, name, filename=None, lineno=-1):
79 """Create the exception
81 :param name: the name of the directive
82 :param filename: the filename of the template
83 :param lineno: the number of line in the template at which the error
84 occurred
85 """
86 TemplateSyntaxError.__init__(self, 'bad directive "%s"' % name,
87 filename, lineno)
90class TemplateRuntimeError(TemplateError):
91 """Exception raised when an the evaluation of a Python expression in a
92 template causes an error.
93 """
96class Context(object):
97 """Container for template input data.
99 A context provides a stack of scopes (represented by dictionaries).
101 Template directives such as loops can push a new scope on the stack with
102 data that should only be available inside the loop. When the loop
103 terminates, that scope can get popped off the stack again.
105 >>> ctxt = Context(one='foo', other=1)
106 >>> ctxt.get('one')
107 'foo'
108 >>> ctxt.get('other')
109 1
110 >>> ctxt.push(dict(one='frost'))
111 >>> ctxt.get('one')
112 'frost'
113 >>> ctxt.get('other')
114 1
115 >>> ctxt.pop()
116 {'one': 'frost'}
117 >>> ctxt.get('one')
118 'foo'
119 """
121 def __init__(self, **data):
122 """Initialize the template context with the given keyword arguments as
123 data.
124 """
125 self.frames = deque([data])
126 self.pop = self.frames.popleft
127 self.push = self.frames.appendleft
128 self._match_templates = []
129 self._choice_stack = []
131 # Helper functions for use in expressions
132 def defined(name):
133 """Return whether a variable with the specified name exists in the
134 expression scope."""
135 return name in self
136 def value_of(name, default=None):
137 """If a variable of the specified name is defined, return its value.
138 Otherwise, return the provided default value, or ``None``."""
139 return self.get(name, default)
140 data.setdefault('defined', defined)
141 data.setdefault('value_of', value_of)
143 def __repr__(self):
144 return repr(list(self.frames))
146 def __contains__(self, key):
147 """Return whether a variable exists in any of the scopes.
149 :param key: the name of the variable
150 """
151 return self._find(key)[1] is not None
152 has_key = __contains__
154 def __delitem__(self, key):
155 """Remove a variable from all scopes.
157 :param key: the name of the variable
158 """
159 for frame in self.frames:
160 if key in frame:
161 del frame[key]
163 def __getitem__(self, key):
164 """Get a variables's value, starting at the current scope and going
165 upward.
167 :param key: the name of the variable
168 :return: the variable value
169 :raises KeyError: if the requested variable wasn't found in any scope
170 """
171 value, frame = self._find(key)
172 if frame is None:
173 raise KeyError(key)
174 return value
176 def __len__(self):
177 """Return the number of distinctly named variables in the context.
179 :return: the number of variables in the context
180 """
181 return len(self.items())
183 def __setitem__(self, key, value):
184 """Set a variable in the current scope.
186 :param key: the name of the variable
187 :param value: the variable value
188 """
189 self.frames[0][key] = value
191 def _find(self, key, default=None):
192 """Retrieve a given variable's value and the frame it was found in.
194 Intended primarily for internal use by directives.
196 :param key: the name of the variable
197 :param default: the default value to return when the variable is not
198 found
199 """
200 for frame in self.frames:
201 if key in frame:
202 return frame[key], frame
203 return default, None
205 def get(self, key, default=None):
206 """Get a variable's value, starting at the current scope and going
207 upward.
209 :param key: the name of the variable
210 :param default: the default value to return when the variable is not
211 found
212 """
213 for frame in self.frames:
214 if key in frame:
215 return frame[key]
216 return default
218 def keys(self):
219 """Return the name of all variables in the context.
221 :return: a list of variable names
222 """
223 keys = []
224 for frame in self.frames:
225 keys += [key for key in frame if key not in keys]
226 return keys
228 def items(self):
229 """Return a list of ``(name, value)`` tuples for all variables in the
230 context.
232 :return: a list of variables
233 """
234 return [(key, self.get(key)) for key in self.keys()]
236 def update(self, mapping):
237 """Update the context from the mapping provided."""
238 self.frames[0].update(mapping)
240 def push(self, data):
241 """Push a new scope on the stack.
243 :param data: the data dictionary to push on the context stack.
244 """
246 def pop(self):
247 """Pop the top-most scope from the stack."""
249 def copy(self):
250 """Create a copy of this Context object."""
251 # required to make f_locals a dict-like object
252 # See http://genshi.edgewall.org/ticket/249 for
253 # example use case in Twisted tracebacks
254 ctxt = Context()
255 ctxt.frames.pop() # pop empty dummy context
256 ctxt.frames.extend(self.frames)
257 ctxt._match_templates.extend(self._match_templates)
258 ctxt._choice_stack.extend(self._choice_stack)
259 return ctxt
262def _apply_directives(stream, directives, ctxt, vars):
263 """Apply the given directives to the stream.
265 :param stream: the stream the directives should be applied to
266 :param directives: the list of directives to apply
267 :param ctxt: the `Context`
268 :param vars: additional variables that should be available when Python
269 code is executed
270 :return: the stream with the given directives applied
271 """
272 if directives:
273 stream = directives[0](iter(stream), directives[1:], ctxt, **vars)
274 return stream
277def _eval_expr(expr, ctxt, vars=None):
278 """Evaluate the given `Expression` object.
280 :param expr: the expression to evaluate
281 :param ctxt: the `Context`
282 :param vars: additional variables that should be available to the
283 expression
284 :return: the result of the evaluation
285 """
286 if vars:
287 ctxt.push(vars)
288 retval = expr.evaluate(ctxt)
289 if vars:
290 ctxt.pop()
291 return retval
294def _exec_suite(suite, ctxt, vars=None):
295 """Execute the given `Suite` object.
297 :param suite: the code suite to execute
298 :param ctxt: the `Context`
299 :param vars: additional variables that should be available to the
300 code
301 """
302 if vars:
303 ctxt.push(vars)
304 ctxt.push({})
305 suite.execute(ctxt)
306 if vars:
307 top = ctxt.pop()
308 ctxt.pop()
309 ctxt.frames[0].update(top)
312class DirectiveFactoryMeta(type):
313 """Meta class for directive factories."""
315 def __new__(cls, name, bases, d):
316 if 'directives' in d:
317 d['_dir_by_name'] = dict(d['directives'])
318 d['_dir_order'] = [directive[1] for directive in d['directives']]
320 return type.__new__(cls, name, bases, d)
323@add_metaclass(DirectiveFactoryMeta)
324class DirectiveFactory(object):
325 """Base for classes that provide a set of template directives.
327 :since: version 0.6
328 """
330 directives = []
331 """A list of ``(name, cls)`` tuples that define the set of directives
332 provided by this factory.
333 """
335 def get_directive(self, name):
336 """Return the directive class for the given name.
338 :param name: the directive name as used in the template
339 :return: the directive class
340 :see: `Directive`
341 """
342 return self._dir_by_name.get(name)
344 def get_directive_index(self, dir_cls):
345 """Return a key for the given directive class that should be used to
346 sort it among other directives on the same `SUB` event.
348 The default implementation simply returns the index of the directive in
349 the `directives` list.
351 :param dir_cls: the directive class
352 :return: the sort key
353 """
354 if dir_cls in self._dir_order:
355 return self._dir_order.index(dir_cls)
356 return len(self._dir_order)
359class Template(DirectiveFactory):
360 """Abstract template base class.
362 This class implements most of the template processing model, but does not
363 specify the syntax of templates.
364 """
366 EXEC = StreamEventKind('EXEC')
367 """Stream event kind representing a Python code suite to execute."""
369 EXPR = StreamEventKind('EXPR')
370 """Stream event kind representing a Python expression."""
372 INCLUDE = StreamEventKind('INCLUDE')
373 """Stream event kind representing the inclusion of another template."""
375 SUB = StreamEventKind('SUB')
376 """Stream event kind representing a nested stream to which one or more
377 directives should be applied.
378 """
380 serializer = None
381 _number_conv = text_type # function used to convert numbers to event data
383 def __init__(self, source, filepath=None, filename=None, loader=None,
384 encoding=None, lookup='strict', allow_exec=True):
385 """Initialize a template from either a string, a file-like object, or
386 an already parsed markup stream.
388 :param source: a string, file-like object, or markup stream to read the
389 template from
390 :param filepath: the absolute path to the template file
391 :param filename: the path to the template file relative to the search
392 path
393 :param loader: the `TemplateLoader` to use for loading included
394 templates
395 :param encoding: the encoding of the `source`
396 :param lookup: the variable lookup mechanism; either "strict" (the
397 default), "lenient", or a custom lookup class
398 :param allow_exec: whether Python code blocks in templates should be
399 allowed
401 :note: Changed in 0.5: Added the `allow_exec` argument
402 """
403 self.filepath = filepath or filename
404 self.filename = filename
405 self.loader = loader
406 self.lookup = lookup
407 self.allow_exec = allow_exec
408 self._init_filters()
409 self._init_loader()
410 self._prepared = False
412 if not isinstance(source, Stream) and not hasattr(source, 'read'):
413 if isinstance(source, text_type):
414 source = StringIO(source)
415 else:
416 source = BytesIO(source)
417 try:
418 self._stream = self._parse(source, encoding)
419 except ParseError as e:
420 raise TemplateSyntaxError(e.msg, self.filepath, e.lineno, e.offset)
422 def __getstate__(self):
423 state = self.__dict__.copy()
424 state['filters'] = []
425 return state
427 def __setstate__(self, state):
428 self.__dict__ = state
429 self._init_filters()
431 def __repr__(self):
432 return '<%s "%s">' % (type(self).__name__, self.filename)
434 def _init_filters(self):
435 self.filters = [self._flatten, self._include]
437 def _init_loader(self):
438 if self.loader is None:
439 from genshi.template.loader import TemplateLoader
440 if self.filename:
441 if self.filepath != self.filename:
442 basedir = os.path.normpath(self.filepath)[:-len(
443 os.path.normpath(self.filename))
444 ]
445 else:
446 basedir = os.path.dirname(self.filename)
447 else:
448 basedir = '.'
449 self.loader = TemplateLoader([os.path.abspath(basedir)])
451 @property
452 def stream(self):
453 if not self._prepared:
454 self._prepare_self()
455 return self._stream
457 def _parse(self, source, encoding):
458 """Parse the template.
460 The parsing stage parses the template and constructs a list of
461 directives that will be executed in the render stage. The input is
462 split up into literal output (text that does not depend on the context
463 data) and directives or expressions.
465 :param source: a file-like object containing the XML source of the
466 template, or an XML event stream
467 :param encoding: the encoding of the `source`
468 """
469 raise NotImplementedError
471 def _prepare_self(self, inlined=None):
472 if not self._prepared:
473 self._stream = list(self._prepare(self._stream, inlined))
474 self._prepared = True
476 def _prepare(self, stream, inlined):
477 """Call the `attach` method of every directive found in the template.
479 :param stream: the event stream of the template
480 """
481 from genshi.template.loader import TemplateNotFound
482 if inlined is None:
483 inlined = set((self.filepath,))
485 for kind, data, pos in stream:
486 if kind is SUB:
487 directives = []
488 substream = data[1]
489 for _, cls, value, namespaces, pos in sorted(
490 data[0], key=lambda x: x[0]):
491 directive, substream = cls.attach(self, substream, value,
492 namespaces, pos)
493 if directive:
494 directives.append(directive)
495 substream = self._prepare(substream, inlined)
496 if directives:
497 yield kind, (directives, list(substream)), pos
498 else:
499 for event in substream:
500 yield event
501 else:
502 if kind is INCLUDE:
503 href, cls, fallback = data
504 tmpl_inlined = False
505 if (isinstance(href, string_types) and
506 not getattr(self.loader, 'auto_reload', True)):
507 # If the path to the included template is static, and
508 # auto-reloading is disabled on the template loader,
509 # the template is inlined into the stream provided it
510 # is not already in the stack of templates being
511 # processed.
512 tmpl = None
513 try:
514 tmpl = self.loader.load(href, relative_to=pos[0],
515 cls=cls or self.__class__)
516 except TemplateNotFound:
517 if fallback is None:
518 raise
519 if tmpl is not None:
520 if tmpl.filepath not in inlined:
521 inlined.add(tmpl.filepath)
522 tmpl._prepare_self(inlined)
523 for event in tmpl.stream:
524 yield event
525 inlined.discard(tmpl.filepath)
526 tmpl_inlined = True
527 else:
528 for event in self._prepare(fallback, inlined):
529 yield event
530 tmpl_inlined = True
531 if tmpl_inlined:
532 continue
533 if fallback:
534 # Otherwise the include is performed at run time
535 data = href, cls, list(
536 self._prepare(fallback, inlined))
537 yield kind, data, pos
538 else:
539 yield kind, data, pos
541 def generate(self, *args, **kwargs):
542 """Apply the template to the given context data.
544 Any keyword arguments are made available to the template as context
545 data.
547 Only one positional argument is accepted: if it is provided, it must be
548 an instance of the `Context` class, and keyword arguments are ignored.
549 This calling style is used for internal processing.
551 :return: a markup event stream representing the result of applying
552 the template to the context data.
553 """
554 vars = {}
555 if args:
556 assert len(args) == 1
557 ctxt = args[0]
558 if ctxt is None:
559 ctxt = Context(**kwargs)
560 else:
561 vars = kwargs
562 assert isinstance(ctxt, Context)
563 else:
564 ctxt = Context(**kwargs)
566 stream = self.stream
567 for filter_ in self.filters:
568 stream = filter_(iter(stream), ctxt, **vars)
569 return Stream(stream, self.serializer)
571 def _flatten(self, stream, ctxt, **vars):
572 number_conv = self._number_conv
573 stack = []
574 push = stack.append
575 pop = stack.pop
576 stream = iter(stream)
578 while 1:
579 for kind, data, pos in stream:
581 if kind is START and data[1]:
582 # Attributes may still contain expressions in start tags at
583 # this point, so do some evaluation
584 tag, attrs = data
585 new_attrs = []
586 for name, value in attrs:
587 if type(value) is list: # this is an interpolated string
588 values = [event[1]
589 for event in self._flatten(value, ctxt, **vars)
590 if event[0] is TEXT and event[1] is not None
591 ]
592 if not values:
593 continue
594 value = ''.join(values)
595 new_attrs.append((name, value))
596 yield kind, (tag, Attrs(new_attrs)), pos
598 elif kind is EXPR:
599 result = _eval_expr(data, ctxt, vars)
600 if result is not None:
601 # First check for a string, otherwise the iterable test
602 # below succeeds, and the string will be chopped up into
603 # individual characters
604 if isinstance(result, string_types):
605 yield TEXT, result, pos
606 elif isinstance(result, numeric_types):
607 yield TEXT, number_conv(result), pos
608 elif hasattr(result, '__iter__'):
609 push(stream)
610 stream = _ensure(result)
611 break
612 else:
613 yield TEXT, text_type(result), pos
615 elif kind is SUB:
616 # This event is a list of directives and a list of nested
617 # events to which those directives should be applied
618 push(stream)
619 stream = _apply_directives(data[1], data[0], ctxt, vars)
620 break
622 elif kind is EXEC:
623 _exec_suite(data, ctxt, vars)
625 else:
626 yield kind, data, pos
628 else:
629 if not stack:
630 break
631 stream = pop()
633 def _include(self, stream, ctxt, **vars):
634 """Internal stream filter that performs inclusion of external
635 template files.
636 """
637 from genshi.template.loader import TemplateNotFound
639 for event in stream:
640 if event[0] is INCLUDE:
641 href, cls, fallback = event[1]
642 if not isinstance(href, string_types):
643 parts = []
644 for subkind, subdata, subpos in self._flatten(href, ctxt,
645 **vars):
646 if subkind is TEXT:
647 parts.append(subdata)
648 href = ''.join([x for x in parts if x is not None])
649 try:
650 tmpl = self.loader.load(href, relative_to=event[2][0],
651 cls=cls or self.__class__)
652 for event in tmpl.generate(ctxt, **vars):
653 yield event
654 except TemplateNotFound:
655 if fallback is None:
656 raise
657 for filter_ in self.filters:
658 fallback = filter_(iter(fallback), ctxt, **vars)
659 for event in fallback:
660 yield event
661 else:
662 yield event
665EXEC = Template.EXEC
666EXPR = Template.EXPR
667INCLUDE = Template.INCLUDE
668SUB = Template.SUB