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