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, \
27 required_ast_call_args, _ast_Str
29__all__ = ['Code', 'Expression', 'Suite', 'LenientLookup', 'StrictLookup',
30 'Undefined', 'UndefinedError']
31__docformat__ = 'restructuredtext en'
36class Code(object):
37 """Abstract base class for the `Expression` and `Suite` classes."""
38 __slots__ = ['source', 'code', 'ast', '_globals']
40 def __init__(self, source, filename=None, lineno=-1, lookup='strict',
41 xform=None):
42 """Create the code object, either from a string, or from an AST node.
44 :param source: either a string containing the source code, or an AST
45 node
46 :param filename: the (preferably absolute) name of the file containing
47 the code
48 :param lineno: the number of the line on which the code was found
49 :param lookup: the lookup class that defines how variables are looked
50 up in the context; can be either "strict" (the default),
51 "lenient", or a custom lookup class
52 :param xform: the AST transformer that should be applied to the code;
53 if `None`, the appropriate transformation is chosen
54 depending on the mode
55 """
56 if isinstance(source, string_types):
57 self.source = source
58 node = _parse(source, mode=self.mode)
59 else:
60 assert isinstance(source, _ast.AST), \
61 'Expected string or AST node, but got %r' % source
62 self.source = '?'
63 if self.mode == 'eval':
64 node = _ast.Expression(body=source)
65 else:
66 node = _ast.Module(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(cls, *args, **kwargs):
457 for attr, value in zip(cls._fields, args):
458 if attr in kwargs:
459 raise ValueError('Field set both in args and kwargs')
460 return cls(*args, **kwargs)
463BUILTINS = builtins.__dict__.copy()
464BUILTINS.update({'Markup': Markup, 'Undefined': Undefined})
465CONSTANTS = frozenset(['False', 'True', 'None', 'NotImplemented', 'Ellipsis'])
468class TemplateASTTransformer(ASTTransformer):
469 """Concrete AST transformer that implements the AST transformations needed
470 for code embedded in templates.
471 """
473 def __init__(self):
474 self.locals = [CONSTANTS]
476 def _process(self, names, node):
477 if not IS_PYTHON2 and isinstance(node, _ast.arg):
478 names.add(node.arg)
479 elif isstring(node):
480 names.add(node)
481 elif isinstance(node, _ast.Name):
482 names.add(node.id)
483 elif isinstance(node, _ast.alias):
484 names.add(node.asname or node.name)
485 elif isinstance(node, _ast.Tuple):
486 for elt in node.elts:
487 self._process(names, elt)
489 def _extract_names(self, node):
490 names = set()
491 if hasattr(node, 'args'):
492 for arg in node.args:
493 self._process(names, arg)
494 if hasattr(node, 'kwonlyargs'):
495 for arg in node.kwonlyargs:
496 self._process(names, arg)
497 if hasattr(node, 'vararg'):
498 self._process(names, node.vararg)
499 if hasattr(node, 'kwarg'):
500 self._process(names, node.kwarg)
501 elif hasattr(node, 'names'):
502 for elt in node.names:
503 self._process(names, elt)
504 return names
506 def visit_Str(self, node):
507 if not isinstance(node.s, text_type):
508 try: # If the string is ASCII, return a `str` object
509 node.s.decode('ascii')
510 except ValueError: # Otherwise return a `unicode` object
511 return _new(_ast_Str, node.s.decode('utf-8'))
512 return node
514 def visit_ClassDef(self, node):
515 if len(self.locals) > 1:
516 self.locals[-1].add(node.name)
517 self.locals.append(set())
518 try:
519 return ASTTransformer.visit_ClassDef(self, node)
520 finally:
521 self.locals.pop()
523 def visit_Import(self, node):
524 if len(self.locals) > 1:
525 self.locals[-1].update(self._extract_names(node))
526 return ASTTransformer.visit_Import(self, node)
528 def visit_ImportFrom(self, node):
529 if [a.name for a in node.names] == ['*']:
530 return node
531 if len(self.locals) > 1:
532 self.locals[-1].update(self._extract_names(node))
533 return ASTTransformer.visit_ImportFrom(self, node)
535 def visit_FunctionDef(self, node):
536 if len(self.locals) > 1:
537 self.locals[-1].add(node.name)
539 self.locals.append(self._extract_names(node.args))
540 try:
541 return ASTTransformer.visit_FunctionDef(self, node)
542 finally:
543 self.locals.pop()
545 # GeneratorExp(expr elt, comprehension* generators)
546 def visit_GeneratorExp(self, node):
547 gens = []
548 for generator in node.generators:
549 # comprehension = (expr target, expr iter, expr* ifs)
550 self.locals.append(set())
551 gen_kw = {}
552 if "is_async" in _ast.comprehension._fields:
553 gen_kw["is_async"] = False
554 gen = _new(_ast.comprehension, self.visit(generator.target),
555 self.visit(generator.iter),
556 [self.visit(if_) for if_ in generator.ifs],
557 **gen_kw)
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], [], *required_ast_call_args)
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, [], *required_ast_call_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, [], *required_ast_call_args)