Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/genshi/template/eval.py: 27%
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"""Support for "safe" evaluation of Python expressions."""
16from textwrap import dedent
17from types import CodeType
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
25from genshi.compat import ast as _ast, _ast_Constant, get_code_params, \
26 build_code_chunk, isstring, IS_PYTHON2, _ast_Str
28__all__ = ['Code', 'Expression', 'Suite', 'LenientLookup', 'StrictLookup',
29 'Undefined', 'UndefinedError']
30__docformat__ = 'restructuredtext en'
34class Code(object):
35 """Abstract base class for the `Expression` and `Suite` classes."""
36 __slots__ = ['source', 'code', 'ast', '_globals']
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.
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]
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
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
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
93 def __eq__(self, other):
94 return (type(other) == type(self)) and (self.code == other.code)
96 def __hash__(self):
97 return hash(self.code)
99 def __ne__(self, other):
100 return not self == other
102 def __repr__(self):
103 return '%s(%r)' % (type(self).__name__, self.source)
106class Expression(Code):
107 """Evaluates Python expressions used in templates.
109 >>> data = dict(test='Foo', items=[1, 2, 3], dict={'some': 'thing'})
110 >>> Expression('test').evaluate(data)
111 'Foo'
113 >>> Expression('items[0]').evaluate(data)
114 1
115 >>> Expression('items[-1]').evaluate(data)
116 3
117 >>> Expression('dict["some"]').evaluate(data)
118 'thing'
120 Similar to e.g. Javascript, expressions in templates can use the dot
121 notation for attribute access to access items in mappings:
123 >>> Expression('dict.some').evaluate(data)
124 'thing'
126 This also works the other way around: item access can be used to access
127 any object attribute:
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'
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:
143 >>> data = dict(items=[1, 2, 3])
144 >>> Expression('len(items)').evaluate(data)
145 3
146 """
147 __slots__ = []
148 mode = 'eval'
150 def evaluate(self, data):
151 """Evaluate the expression against the given data dictionary.
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})
161class Suite(Code):
162 """Executes Python statements used in templates.
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'
172 def execute(self, data):
173 """Execute the suite in the given data dictionary.
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)
182UNDEFINED = object()
185class UndefinedError(TemplateRuntimeError):
186 """Exception thrown when a template expression attempts to access a variable
187 not defined in the context.
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)
199class Undefined(object):
200 """Represents a reference to an undefined variable.
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:
207 >>> foo = Undefined('foo')
208 >>> bool(foo)
209 False
210 >>> list(foo)
211 []
212 >>> print(foo)
213 undefined
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.
219 >>> try:
220 ... foo('bar')
221 ... except UndefinedError as e:
222 ... print(e.msg)
223 "foo" not defined
225 >>> try:
226 ... foo.bar
227 ... except UndefinedError as e:
228 ... print(e.msg)
229 "foo" not defined
231 :see: `LenientLookup`
232 """
233 __slots__ = ['_name', '_owner']
235 def __init__(self, name, owner=UNDEFINED):
236 """Initialize the object.
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
244 def __iter__(self):
245 return iter([])
247 def __bool__(self):
248 return False
249 # Python 2
250 __nonzero__ = __bool__
252 def __repr__(self):
253 return '<%s %r>' % (type(self).__name__, self._name)
255 def __str__(self):
256 return 'undefined'
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
264 # Hack around some behavior introduced in Python 2.6.2
265 # http://genshi.edgewall.org/ticket/324
266 __length_hint__ = None
269class LookupBase(object):
270 """Abstract base class for variable lookup implementations."""
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 }
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
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
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
325 @classmethod
326 def undefined(cls, key, owner=UNDEFINED):
327 """Can be overridden by subclasses to specify behavior when undefined
328 variables are accessed.
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
336class LenientLookup(LookupBase):
337 """Default variable lookup mechanism for expressions.
339 When an undefined variable is referenced using this lookup style, the
340 reference evaluates to an instance of the `Undefined` class:
342 >>> expr = Expression('nothing', lookup='lenient')
343 >>> undef = expr.evaluate({})
344 >>> undef
345 <Undefined 'nothing'>
347 The same will happen when a non-existing attribute or item is accessed on
348 an existing object:
350 >>> expr = Expression('something.nil', lookup='lenient')
351 >>> expr.evaluate({'something': dict()})
352 <Undefined 'nil'>
354 See the documentation of the `Undefined` class for details on the behavior
355 of such objects.
357 :see: `StrictLookup`
358 """
360 @classmethod
361 def undefined(cls, key, owner=UNDEFINED):
362 """Return an ``Undefined`` object."""
363 __traceback_hide__ = True
364 return Undefined(key, owner=owner)
367class StrictLookup(LookupBase):
368 """Strict variable lookup mechanism for expressions.
370 Referencing an undefined variable using this lookup style will immediately
371 raise an ``UndefinedError``:
373 >>> expr = Expression('nothing', lookup='strict')
374 >>> try:
375 ... expr.evaluate({})
376 ... except UndefinedError as e:
377 ... print(e.msg)
378 "nothing" not defined
380 The same happens when a non-existing attribute or item is accessed on an
381 existing object:
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 """
391 @classmethod
392 def undefined(cls, key, owner=UNDEFINED):
393 """Raise an ``UndefinedError`` immediately."""
394 __traceback_hide__ = True
395 raise UndefinedError(key, owner=owner)
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)
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
428 if xform is None:
429 xform = {
430 'eval': ExpressionASTTransformer
431 }.get(mode, TemplateASTTransformer)
432 tree = xform().visit(node)
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)
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
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
467BUILTINS = builtins.__dict__.copy()
468BUILTINS.update({'Markup': Markup, 'Undefined': Undefined})
469CONSTANTS = frozenset(['False', 'True', 'None', 'NotImplemented', 'Ellipsis'])
472class TemplateASTTransformer(ASTTransformer):
473 """Concrete AST transformer that implements the AST transformations needed
474 for code embedded in templates.
475 """
477 def __init__(self):
478 self.locals = [CONSTANTS]
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)
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
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
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()
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)
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)
539 def visit_FunctionDef(self, node):
540 if len(self.locals) > 1:
541 self.locals[-1].add(node.name)
543 self.locals.append(self._extract_names(node.args))
544 try:
545 return ASTTransformer.visit_FunctionDef(self, node)
546 finally:
547 self.locals.pop()
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)
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
566 # ListComp(expr elt, comprehension* generators)
567 visit_ListComp = visit_GeneratorExp
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()
576 # Only used in Python 3.5+
577 def visit_Starred(self, node):
578 node.value = self.visit(node.value)
579 return node
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)
595 return node
598class ExpressionASTTransformer(TemplateASTTransformer):
599 """Concrete AST transformer that implements the AST transformations needed
600 for code embedded in templates.
601 """
603 def visit_Attribute(self, node):
604 if not isinstance(node.ctx, _ast.Load):
605 return ASTTransformer.visit_Attribute(self, node)
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, [])
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)
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
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, [])