added loop filtering
[jinja2.git] / jinja2 / runtime.py
1 # -*- coding: utf-8 -*-
2 """
3     jinja2.runtime
4     ~~~~~~~~~~~~~~
5
6     Runtime helpers.
7
8     :copyright: Copyright 2008 by Armin Ronacher.
9     :license: GNU GPL.
10 """
11 try:
12     from collections import defaultdict
13 except ImportError:
14     defaultdict = None
15
16
17 __all__ = ['subscribe', 'LoopContext', 'StaticLoopContext', 'TemplateContext',
18            'Macro', 'IncludedTemplate', 'Undefined']
19
20
21 def subscribe(obj, argument):
22     """Get an item or attribute of an object."""
23     try:
24         return getattr(obj, str(argument))
25     except (AttributeError, UnicodeError):
26         try:
27             return obj[argument]
28         except (TypeError, LookupError):
29             return Undefined(obj, argument)
30
31
32 class TemplateData(unicode):
33     """Marks data as "coming from the template".  This is used to let the
34     system know that this data is already processed if a finalization is
35     used."""
36
37     def __html__(self):
38         return self
39
40
41 class TemplateContext(dict):
42     """Holds the variables of the local template or of the global one.  It's
43     not save to use this class outside of the compiled code.  For example
44     update and other methods will not work as they seem (they don't update
45     the exported variables for example).
46     """
47
48     def __init__(self, globals, filename, blocks, standalone):
49         dict.__init__(self, globals)
50         self.exported = set()
51         self.filename = filename
52         self.blocks = dict((k, [v]) for k, v in blocks.iteritems())
53
54         # if the template is in standalone mode we don't copy the blocks over.
55         # this is used for includes for example but otherwise, if the globals
56         # are a template context, this template is participating in a template
57         # inheritance chain and we have to copy the blocks over.
58         if not standalone and isinstance(globals, TemplateContext):
59                 for name, parent_blocks in globals.blocks.iteritems():
60                     self.blocks.setdefault(name, []).extend(parent_blocks)
61
62     def __setitem__(self, key, value):
63         """If we set items to the dict we track the variables set so
64         that includes can access the exported variables."""
65         dict.__setitem__(self, key, value)
66         self.exported.add(key)
67
68     def __delitem__(self, key):
69         """On delete we no longer export it."""
70         dict.__delitem__(self, key)
71         self.exported.dicard(key)
72
73     def get_exported(self):
74         """Get a dict of all exported variables."""
75         return dict((k, self[k]) for k in self.exported)
76
77     # if there is a default dict, dict has a __missing__ method we can use.
78     if defaultdict is None:
79         def __getitem__(self, name):
80             if name in self:
81                 return self[name]
82             return Undefined(name)
83     else:
84         def __missing__(self, key):
85             return Undefined(key)
86
87     def __repr__(self):
88         return '<%s %s of %r>' % (
89             self.__class__.__name__,
90             dict.__repr__(self),
91             self.filename
92         )
93
94
95 class IncludedTemplate(object):
96     """Represents an included template."""
97
98     def __init__(self, environment, context, template):
99         template = environment.get_template(template)
100         gen = template.root_render_func(context, standalone=True)
101         context = gen.next()
102         self._filename = template.name
103         self._rendered_body = u''.join(gen)
104         self._context = context.get_exported()
105
106     def __getitem__(self, name):
107         return self._context[name]
108
109     def __unicode__(self):
110         return self._context
111
112     def __repr__(self):
113         return '<%s %r>' % (
114             self.__class__.__name__,
115             self._filename
116         )
117
118
119 class LoopContextBase(object):
120     """Helper for extended iteration."""
121
122     def __init__(self, iterable, parent=None):
123         self._iterable = iterable
124         self._length = None
125         self.index0 = 0
126         self.parent = parent
127
128     first = property(lambda x: x.index0 == 0)
129     last = property(lambda x: x.revindex0 == 0)
130     index = property(lambda x: x.index0 + 1)
131     revindex = property(lambda x: x.length)
132     revindex0 = property(lambda x: x.length - 1)
133     length = property(lambda x: len(x))
134
135
136 class LoopContext(LoopContextBase):
137     """A loop context for dynamic iteration."""
138
139     def __init__(self, iterable, parent=None, enforce_length=False):
140         self._iterable = iterable
141         self._next = iter(iterable).next
142         self._length = None
143         self.index0 = -1
144         self.parent = parent
145         if enforce_length:
146             len(self)
147
148     def make_static(self):
149         """Return a static loop context for the optimizer."""
150         parent = None
151         if self.parent is not None:
152             parent = self.parent.make_static()
153         return StaticLoopContext(self.index0, self.length, parent)
154
155     def __iter__(self):
156         return self
157
158     def next(self):
159         self.index0 += 1
160         return self._next(), self
161
162     def __len__(self):
163         if self._length is None:
164             try:
165                 length = len(self._iterable)
166             except TypeError:
167                 self._iterable = tuple(self._iterable)
168                 self._next = iter(self._iterable).next
169                 length = len(tuple(self._iterable)) + self.index0 + 1
170             self._length = length
171         return self._length
172
173
174 class StaticLoopContext(LoopContextBase):
175     """The static loop context is used in the optimizer to "freeze" the
176     status of an iteration.  The only reason for this object is if the
177     loop object is accessed in a non static way (eg: becomes part of a
178     function call)."""
179
180     def __init__(self, index0, length, parent):
181         self.index0 = index0
182         self.parent = parent
183         self._length = length
184
185     def __repr__(self):
186         """The repr is used by the optimizer to dump the object."""
187         return 'StaticLoopContext(%r, %r, %r)' % (
188             self.index0,
189             self._length,
190             self.parent
191         )
192
193     def make_static(self):
194         return self
195
196
197 class Macro(object):
198     """Wraps a macro."""
199
200     def __init__(self, func, name, arguments, defaults, catch_all, caller):
201         self._func = func
202         self.name = name
203         self.arguments = arguments
204         self.defaults = defaults
205         self.catch_all = catch_all
206         self.caller = caller
207
208     def __call__(self, *args, **kwargs):
209         arg_count = len(self.arguments)
210         if len(args) > arg_count:
211             raise TypeError('macro %r takes not more than %d argument(s).' %
212                             (self.name, len(self.arguments)))
213         arguments = {}
214         for idx, name in enumerate(self.arguments):
215             try:
216                 value = args[idx]
217             except IndexError:
218                 try:
219                     value = kwargs.pop(name)
220                 except KeyError:
221                     try:
222                         value = self.defaults[idx - arg_count]
223                     except IndexError:
224                         value = Undefined(name, extra='parameter not provided')
225             arguments['l_' + name] = value
226         if self.caller:
227             caller = kwargs.pop('caller', None)
228             if caller is None:
229                 caller = Undefined('caller', extra='The macro was called '
230                                    'from an expression and not a call block.')
231             arguments['l_caller'] = caller
232         if self.catch_all:
233             arguments['l_arguments'] = kwargs
234         return TemplateData(u''.join(self._func(**arguments)))
235
236     def __repr__(self):
237         return '<%s %s>' % (
238             self.__class__.__name__,
239             self.name is None and 'anonymous' or repr(self.name)
240         )
241
242
243 class Undefined(object):
244     """The object for undefined values."""
245
246     def __init__(self, name=None, attr=None, extra=None):
247         if attr is None:
248             self._undefined_hint = '%r is undefined' % name
249         else:
250             self._undefined_hint = 'attribute %r of %r is undefined' \
251                                    % (attr, name)
252         if extra is not None:
253             self._undefined_hint += ' (' + extra + ')'
254
255     def fail(self, *args, **kwargs):
256         raise TypeError(self._undefined_hint)
257     __getattr__ = __getitem__ = __add__ = __mul__ = __div__ = \
258     __realdiv__ = __floordiv__ = __mod__ = __pos__ = __neg__ = \
259     __call__ = fail
260     del fail
261
262     def __unicode__(self):
263         return ''
264
265     def __repr__(self):
266         return 'Undefined'
267
268     def __len__(self):
269         return 0
270
271     def __iter__(self):
272         if 0:
273             yield None