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/.
13
14"""Support for "safe" evaluation of Python expressions."""
15
16from textwrap import dedent
17from types import CodeType
18
19from genshi.compat import builtins, exec_, string_types, text_type
20from genshi.core import Markup
21from genshi.template.astutil import ASTTransformer, ASTCodeGenerator, parse
22from genshi.template.base import TemplateRuntimeError
23from genshi.util import flatten
24
25from genshi.compat import ast as _ast, _ast_Constant, get_code_params, \
26 build_code_chunk, isstring, IS_PYTHON2, _ast_Str
27
28__all__ = ['Code', 'Expression', 'Suite', 'LenientLookup', 'StrictLookup',
29 'Undefined', 'UndefinedError']
30__docformat__ = 'restructuredtext en'
31
32
33
34class Code(object):
35 """Abstract base class for the `Expression` and `Suite` classes."""
36 __slots__ = ['source', 'code', 'ast', '_globals']
37
38 def __init__(self, source, filename=None, lineno=-1, lookup='strict',
39 xform=None):
40 """Create the code object, either from a string, or from an AST node.
41
42 :param source: either a string containing the source code, or an AST
43 node
44 :param filename: the (preferably absolute) name of the file containing
45 the code
46 :param lineno: the number of the line on which the code was found
47 :param lookup: the lookup class that defines how variables are looked
48 up in the context; can be either "strict" (the default),
49 "lenient", or a custom lookup class
50 :param xform: the AST transformer that should be applied to the code;
51 if `None`, the appropriate transformation is chosen
52 depending on the mode
53 """
54 if isinstance(source, string_types):
55 self.source = source
56 node = _parse(source, mode=self.mode)
57 else:
58 assert isinstance(source, _ast.AST), \
59 'Expected string or AST node, but got %r' % source
60 self.source = '?'
61 if self.mode == 'eval':
62 node = _ast.Expression()
63 node.body = source
64 else:
65 node = _ast.Module()
66 node.body = [source]
67
68 self.ast = node
69 self.code = _compile(node, self.source, mode=self.mode,
70 filename=filename, lineno=lineno, xform=xform)
71 if lookup is None:
72 lookup = LenientLookup
73 elif isinstance(lookup, string_types):
74 lookup = {'lenient': LenientLookup, 'strict': StrictLookup}[lookup]
75 self._globals = lookup.globals
76
77 def __getstate__(self):
78 if hasattr(self._globals, '__self__'):
79 # Python 3
80 lookup_fn = self._globals.__self__
81 else:
82 lookup_fn = self._globals.im_self
83 state = {'source': self.source, 'ast': self.ast, 'lookup': lookup_fn}
84 state['code'] = get_code_params(self.code)
85 return state
86
87 def __setstate__(self, state):
88 self.source = state['source']
89 self.ast = state['ast']
90 self.code = CodeType(0, *state['code'])
91 self._globals = state['lookup'].globals
92
93 def __eq__(self, other):
94 return (type(other) == type(self)) and (self.code == other.code)
95
96 def __hash__(self):
97 return hash(self.code)
98
99 def __ne__(self, other):
100 return not self == other
101
102 def __repr__(self):
103 return '%s(%r)' % (type(self).__name__, self.source)
104
105
106class Expression(Code):
107 """Evaluates Python expressions used in templates.
108
109 >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
110 >>> Expression('test').evaluate(data)
111 'Foo'
112
113 >>> Expression('items[0]').evaluate(data)
114 1
115 >>> Expression('items[-1]').evaluate(data)
116 3
117 >>> Expression('dict["some"]').evaluate(data)
118 'thing'
119
120 Similar to e.g. Javascript, expressions in templates can use the dot
121 notation for attribute access to access items in mappings:
122
123 >>> Expression('dict.some').evaluate(data)
124 'thing'
125
126 This also works the other way around: item access can be used to access
127 any object attribute:
128
129 >>> class MyClass(object):
130 ... myattr = 'Bar'
131 >>> data = dict(mine=MyClass(), key='myattr')
132 >>> Expression('mine.myattr').evaluate(data)
133 'Bar'
134 >>> Expression('mine["myattr"]').evaluate(data)
135 'Bar'
136 >>> Expression('mine[key]').evaluate(data)
137 'Bar'
138
139 All of the standard Python operators are available to template expressions.
140 Built-in functions such as ``len()`` are also available in template
141 expressions:
142
143 >>> data = dict(items=[1, 2, 3])
144 >>> Expression('len(items)').evaluate(data)
145 3
146 """
147 __slots__ = []
148 mode = 'eval'
149
150 def evaluate(self, data):
151 """Evaluate the expression against the given data dictionary.
152
153 :param data: a mapping containing the data to evaluate against
154 :return: the result of the evaluation
155 """
156 __traceback_hide__ = 'before_and_this'
157 _globals = self._globals(data)
158 return eval(self.code, _globals, {'__data__': data})
159
160
161class Suite(Code):
162 """Executes Python statements used in templates.
163
164 >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
165 >>> Suite("foo = dict['some']").execute(data)
166 >>> data['foo']
167 'thing'
168 """
169 __slots__ = []
170 mode = 'exec'
171
172 def execute(self, data):
173 """Execute the suite in the given data dictionary.
174
175 :param data: a mapping containing the data to execute in
176 """
177 __traceback_hide__ = 'before_and_this'
178 _globals = self._globals(data)
179 exec_(self.code, _globals, data)
180
181
182UNDEFINED = object()
183
184
185class UndefinedError(TemplateRuntimeError):
186 """Exception thrown when a template expression attempts to access a variable
187 not defined in the context.
188
189 :see: `LenientLookup`, `StrictLookup`
190 """
191 def __init__(self, name, owner=UNDEFINED):
192 if owner is not UNDEFINED:
193 message = '%s has no member named "%s"' % (repr(owner), name)
194 else:
195 message = '"%s" not defined' % name
196 TemplateRuntimeError.__init__(self, message)
197
198
199class Undefined(object):
200 """Represents a reference to an undefined variable.
201
202 Unlike the Python runtime, template expressions can refer to an undefined
203 variable without causing a `NameError` to be raised. The result will be an
204 instance of the `Undefined` class, which is treated the same as ``False`` in
205 conditions, but raise an exception on any other operation:
206
207 >>> foo = Undefined('foo')
208 >>> bool(foo)
209 False
210 >>> list(foo)
211 []
212 >>> print(foo)
213 undefined
214
215 However, calling an undefined variable, or trying to access an attribute
216 of that variable, will raise an exception that includes the name used to
217 reference that undefined variable.
218
219 >>> try:
220 ... foo('bar')
221 ... except UndefinedError as e:
222 ... print(e.msg)
223 "foo" not defined
224
225 >>> try:
226 ... foo.bar
227 ... except UndefinedError as e:
228 ... print(e.msg)
229 "foo" not defined
230
231 :see: `LenientLookup`
232 """
233 __slots__ = ['_name', '_owner']
234
235 def __init__(self, name, owner=UNDEFINED):
236 """Initialize the object.
237
238 :param name: the name of the reference
239 :param owner: the owning object, if the variable is accessed as a member
240 """
241 self._name = name
242 self._owner = owner
243
244 def __iter__(self):
245 return iter([])
246
247 def __bool__(self):
248 return False
249 # Python 2
250 __nonzero__ = __bool__
251
252 def __repr__(self):
253 return '<%s %r>' % (type(self).__name__, self._name)
254
255 def __str__(self):
256 return 'undefined'
257
258 def _die(self, *args, **kwargs):
259 """Raise an `UndefinedError`."""
260 __traceback_hide__ = True
261 raise UndefinedError(self._name, self._owner)
262 __call__ = __getattr__ = __getitem__ = _die
263
264 # Hack around some behavior introduced in Python 2.6.2
265 # http://genshi.edgewall.org/ticket/324
266 __length_hint__ = None
267
268
269class LookupBase(object):
270 """Abstract base class for variable lookup implementations."""
271
272 @classmethod
273 def globals(cls, data):
274 """Construct the globals dictionary to use as the execution context for
275 the expression or suite.
276 """
277 return {
278 '__data__': data,
279 '_lookup_name': cls.lookup_name,
280 '_lookup_attr': cls.lookup_attr,
281 '_lookup_item': cls.lookup_item,
282 'UndefinedError': UndefinedError,
283 }
284
285 @classmethod
286 def lookup_name(cls, data, name):
287 __traceback_hide__ = True
288 val = data.get(name, UNDEFINED)
289 if val is UNDEFINED:
290 val = BUILTINS.get(name, val)
291 if val is UNDEFINED:
292 val = cls.undefined(name)
293 return val
294
295 @classmethod
296 def lookup_attr(cls, obj, key):
297 __traceback_hide__ = True
298 try:
299 val = getattr(obj, key)
300 except AttributeError:
301 if hasattr(obj.__class__, key):
302 raise
303 else:
304 try:
305 val = obj[key]
306 except (KeyError, TypeError):
307 val = cls.undefined(key, owner=obj)
308 return val
309
310 @classmethod
311 def lookup_item(cls, obj, key):
312 __traceback_hide__ = True
313 if len(key) == 1:
314 key = key[0]
315 try:
316 return obj[key]
317 except (AttributeError, KeyError, IndexError, TypeError) as e:
318 if isinstance(key, string_types):
319 val = getattr(obj, key, UNDEFINED)
320 if val is UNDEFINED:
321 val = cls.undefined(key, owner=obj)
322 return val
323 raise
324
325 @classmethod
326 def undefined(cls, key, owner=UNDEFINED):
327 """Can be overridden by subclasses to specify behavior when undefined
328 variables are accessed.
329
330 :param key: the name of the variable
331 :param owner: the owning object, if the variable is accessed as a member
332 """
333 raise NotImplementedError
334
335
336class LenientLookup(LookupBase):
337 """Default variable lookup mechanism for expressions.
338
339 When an undefined variable is referenced using this lookup style, the
340 reference evaluates to an instance of the `Undefined` class:
341
342 >>> expr = Expression('nothing', lookup='lenient')
343 >>> undef = expr.evaluate({})
344 >>> undef
345 <Undefined 'nothing'>
346
347 The same will happen when a non-existing attribute or item is accessed on
348 an existing object:
349
350 >>> expr = Expression('something.nil', lookup='lenient')
351 >>> expr.evaluate({'something': dict()})
352 <Undefined 'nil'>
353
354 See the documentation of the `Undefined` class for details on the behavior
355 of such objects.
356
357 :see: `StrictLookup`
358 """
359
360 @classmethod
361 def undefined(cls, key, owner=UNDEFINED):
362 """Return an ``Undefined`` object."""
363 __traceback_hide__ = True
364 return Undefined(key, owner=owner)
365
366
367class StrictLookup(LookupBase):
368 """Strict variable lookup mechanism for expressions.
369
370 Referencing an undefined variable using this lookup style will immediately
371 raise an ``UndefinedError``:
372
373 >>> expr = Expression('nothing', lookup='strict')
374 >>> try:
375 ... expr.evaluate({})
376 ... except UndefinedError as e:
377 ... print(e.msg)
378 "nothing" not defined
379
380 The same happens when a non-existing attribute or item is accessed on an
381 existing object:
382
383 >>> expr = Expression('something.nil', lookup='strict')
384 >>> try:
385 ... expr.evaluate({'something': dict()})
386 ... except UndefinedError as e:
387 ... print(e.msg)
388 {} has no member named "nil"
389 """
390
391 @classmethod
392 def undefined(cls, key, owner=UNDEFINED):
393 """Raise an ``UndefinedError`` immediately."""
394 __traceback_hide__ = True
395 raise UndefinedError(key, owner=owner)
396
397
398def _parse(source, mode='eval'):
399 source = source.strip()
400 if mode == 'exec':
401 lines = [line.expandtabs() for line in source.splitlines()]
402 if lines:
403 first = lines[0]
404 rest = dedent('\n'.join(lines[1:])).rstrip()
405 if first.rstrip().endswith(':') and not rest[0].isspace():
406 rest = '\n'.join([' %s' % line for line in rest.splitlines()])
407 source = '\n'.join([first, rest])
408 if isinstance(source, text_type):
409 source = (u'\ufeff' + source).encode('utf-8')
410 return parse(source, mode)
411
412
413def _compile(node, source=None, mode='eval', filename=None, lineno=-1,
414 xform=None):
415 if not filename:
416 filename = '<string>'
417 if IS_PYTHON2:
418 # Python 2 requires non-unicode filenames
419 if isinstance(filename, text_type):
420 filename = filename.encode('utf-8', 'replace')
421 else:
422 # Python 3 requires unicode filenames
423 if not isinstance(filename, text_type):
424 filename = filename.decode('utf-8', 'replace')
425 if lineno <= 0:
426 lineno = 1
427
428 if xform is None:
429 xform = {
430 'eval': ExpressionASTTransformer
431 }.get(mode, TemplateASTTransformer)
432 tree = xform().visit(node)
433
434 if mode == 'eval':
435 name = '<Expression %r>' % (source or '?')
436 else:
437 lines = source.splitlines()
438 if not lines:
439 extract = ''
440 else:
441 extract = lines[0]
442 if len(lines) > 1:
443 extract += ' ...'
444 name = '<Suite %r>' % (extract)
445 new_source = ASTCodeGenerator(tree).code
446 code = compile(new_source, filename, mode)
447
448 try:
449 # We'd like to just set co_firstlineno, but it's readonly. So we need
450 # to clone the code object while adjusting the line number
451 return build_code_chunk(code, filename, name, lineno)
452 except RuntimeError:
453 return code
454
455
456def _new(class_, *args, **kwargs):
457 ret = class_()
458 for attr, value in zip(ret._fields, args):
459 if attr in kwargs:
460 raise ValueError('Field set both in args and kwargs')
461 setattr(ret, attr, value)
462 for attr, value in kwargs:
463 setattr(ret, attr, value)
464 return ret
465
466
467BUILTINS = builtins.__dict__.copy()
468BUILTINS.update({'Markup': Markup, 'Undefined': Undefined})
469CONSTANTS = frozenset(['False', 'True', 'None', 'NotImplemented', 'Ellipsis'])
470
471
472class TemplateASTTransformer(ASTTransformer):
473 """Concrete AST transformer that implements the AST transformations needed
474 for code embedded in templates.
475 """
476
477 def __init__(self):
478 self.locals = [CONSTANTS]
479
480 def _process(self, names, node):
481 if not IS_PYTHON2 and isinstance(node, _ast.arg):
482 names.add(node.arg)
483 elif isstring(node):
484 names.add(node)
485 elif isinstance(node, _ast.Name):
486 names.add(node.id)
487 elif isinstance(node, _ast.alias):
488 names.add(node.asname or node.name)
489 elif isinstance(node, _ast.Tuple):
490 for elt in node.elts:
491 self._process(names, elt)
492
493 def _extract_names(self, node):
494 names = set()
495 if hasattr(node, 'args'):
496 for arg in node.args:
497 self._process(names, arg)
498 if hasattr(node, 'kwonlyargs'):
499 for arg in node.kwonlyargs:
500 self._process(names, arg)
501 if hasattr(node, 'vararg'):
502 self._process(names, node.vararg)
503 if hasattr(node, 'kwarg'):
504 self._process(names, node.kwarg)
505 elif hasattr(node, 'names'):
506 for elt in node.names:
507 self._process(names, elt)
508 return names
509
510 def visit_Str(self, node):
511 if not isinstance(node.s, text_type):
512 try: # If the string is ASCII, return a `str` object
513 node.s.decode('ascii')
514 except ValueError: # Otherwise return a `unicode` object
515 return _new(_ast_Str, node.s.decode('utf-8'))
516 return node
517
518 def visit_ClassDef(self, node):
519 if len(self.locals) > 1:
520 self.locals[-1].add(node.name)
521 self.locals.append(set())
522 try:
523 return ASTTransformer.visit_ClassDef(self, node)
524 finally:
525 self.locals.pop()
526
527 def visit_Import(self, node):
528 if len(self.locals) > 1:
529 self.locals[-1].update(self._extract_names(node))
530 return ASTTransformer.visit_Import(self, node)
531
532 def visit_ImportFrom(self, node):
533 if [a.name for a in node.names] == ['*']:
534 return node
535 if len(self.locals) > 1:
536 self.locals[-1].update(self._extract_names(node))
537 return ASTTransformer.visit_ImportFrom(self, node)
538
539 def visit_FunctionDef(self, node):
540 if len(self.locals) > 1:
541 self.locals[-1].add(node.name)
542
543 self.locals.append(self._extract_names(node.args))
544 try:
545 return ASTTransformer.visit_FunctionDef(self, node)
546 finally:
547 self.locals.pop()
548
549 # GeneratorExp(expr elt, comprehension* generators)
550 def visit_GeneratorExp(self, node):
551 gens = []
552 for generator in node.generators:
553 # comprehension = (expr target, expr iter, expr* ifs)
554 self.locals.append(set())
555 gen = _new(_ast.comprehension, self.visit(generator.target),
556 self.visit(generator.iter),
557 [self.visit(if_) for if_ in generator.ifs])
558 gens.append(gen)
559
560 # use node.__class__ to make it reusable as ListComp
561 ret = _new(node.__class__, self.visit(node.elt), gens)
562 #delete inserted locals
563 del self.locals[-len(node.generators):]
564 return ret
565
566 # ListComp(expr elt, comprehension* generators)
567 visit_ListComp = visit_GeneratorExp
568
569 def visit_Lambda(self, node):
570 self.locals.append(self._extract_names(node.args))
571 try:
572 return ASTTransformer.visit_Lambda(self, node)
573 finally:
574 self.locals.pop()
575
576 # Only used in Python 3.5+
577 def visit_Starred(self, node):
578 node.value = self.visit(node.value)
579 return node
580
581 def visit_Name(self, node):
582 # If the name refers to a local inside a lambda, list comprehension, or
583 # generator expression, leave it alone
584 if isinstance(node.ctx, _ast.Load) and \
585 node.id not in flatten(self.locals):
586 # Otherwise, translate the name ref into a context lookup
587 name = _new(_ast.Name, '_lookup_name', _ast.Load())
588 namearg = _new(_ast.Name, '__data__', _ast.Load())
589 strarg = _new(_ast_Str, node.id)
590 node = _new(_ast.Call, name, [namearg, strarg], [])
591 elif isinstance(node.ctx, _ast.Store):
592 if len(self.locals) > 1:
593 self.locals[-1].add(node.id)
594
595 return node
596
597
598class ExpressionASTTransformer(TemplateASTTransformer):
599 """Concrete AST transformer that implements the AST transformations needed
600 for code embedded in templates.
601 """
602
603 def visit_Attribute(self, node):
604 if not isinstance(node.ctx, _ast.Load):
605 return ASTTransformer.visit_Attribute(self, node)
606
607 func = _new(_ast.Name, '_lookup_attr', _ast.Load())
608 args = [self.visit(node.value), _new(_ast_Str, node.attr)]
609 return _new(_ast.Call, func, args, [])
610
611 def visit_Subscript(self, node):
612 if not isinstance(node.ctx, _ast.Load) or \
613 not isinstance(node.slice, (_ast.Index, _ast_Constant, _ast.Name, _ast.Call)):
614 return ASTTransformer.visit_Subscript(self, node)
615
616 # Before Python 3.9 "foo[key]" wrapped the load of "key" in
617 # "ast.Index(ast.Name(...))"
618 if isinstance(node.slice, (_ast.Name, _ast.Call)):
619 slice_value = node.slice
620 else:
621 slice_value = node.slice.value
622
623
624 func = _new(_ast.Name, '_lookup_item', _ast.Load())
625 args = [
626 self.visit(node.value),
627 _new(_ast.Tuple, (self.visit(slice_value),), _ast.Load())
628 ]
629 return _new(_ast.Call, func, args, [])