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

304 statements  

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, [])