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 | |
---|
16 | import __builtin__ |
---|
17 | |
---|
18 | from textwrap import dedent |
---|
19 | from types import CodeType |
---|
20 | |
---|
21 | from genshi.core import Markup |
---|
22 | from genshi.template.astutil import ASTTransformer, ASTCodeGenerator, \ |
---|
23 | _ast, parse |
---|
24 | from genshi.template.base import TemplateRuntimeError |
---|
25 | from genshi.util import flatten |
---|
26 | |
---|
27 | from genshi.compat import get_code_params, build_code_chunk, isstring, \ |
---|
28 | IS_PYTHON2 |
---|
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 |
---|
36 | has_star_import_bug = False |
---|
37 | try: |
---|
38 | class _FakeMapping(object): |
---|
39 | __getitem__ = __setitem__ = lambda *a: None |
---|
40 | exec 'from sys import *' in {}, _FakeMapping() |
---|
41 | except SystemError: |
---|
42 | has_star_import_bug = True |
---|
43 | del _FakeMapping |
---|
44 | |
---|
45 | |
---|
46 | def _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 | |
---|
58 | class 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 | |
---|
126 | class 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 | |
---|
181 | class 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 | |
---|
202 | UNDEFINED = object() |
---|
203 | |
---|
204 | |
---|
205 | class 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 | |
---|
219 | class 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 | |
---|
287 | class 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 | |
---|
355 | class 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 | |
---|
386 | class 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 | |
---|
417 | def _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 | |
---|
432 | def _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 | |
---|
475 | def _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 | |
---|
486 | BUILTINS = __builtin__.__dict__.copy() |
---|
487 | BUILTINS.update({'Markup': Markup, 'Undefined': Undefined}) |
---|
488 | CONSTANTS = frozenset(['False', 'True', 'None', 'NotImplemented', 'Ellipsis']) |
---|
489 | |
---|
490 | |
---|
491 | class 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 | def visit_Name(self, node): |
---|
604 | # If the name refers to a local inside a lambda, list comprehension, or |
---|
605 | # generator expression, leave it alone |
---|
606 | if isinstance(node.ctx, _ast.Load) and \ |
---|
607 | node.id not in flatten(self.locals): |
---|
608 | # Otherwise, translate the name ref into a context lookup |
---|
609 | name = _new(_ast.Name, '_lookup_name', _ast.Load()) |
---|
610 | namearg = _new(_ast.Name, '__data__', _ast.Load()) |
---|
611 | strarg = _new(_ast.Str, node.id) |
---|
612 | node = _new(_ast.Call, name, [namearg, strarg], []) |
---|
613 | elif isinstance(node.ctx, _ast.Store): |
---|
614 | if len(self.locals) > 1: |
---|
615 | self.locals[-1].add(node.id) |
---|
616 | |
---|
617 | return node |
---|
618 | |
---|
619 | |
---|
620 | class ExpressionASTTransformer(TemplateASTTransformer): |
---|
621 | """Concrete AST transformer that implements the AST transformations needed |
---|
622 | for code embedded in templates. |
---|
623 | """ |
---|
624 | |
---|
625 | def visit_Attribute(self, node): |
---|
626 | if not isinstance(node.ctx, _ast.Load): |
---|
627 | return ASTTransformer.visit_Attribute(self, node) |
---|
628 | |
---|
629 | func = _new(_ast.Name, '_lookup_attr', _ast.Load()) |
---|
630 | args = [self.visit(node.value), _new(_ast.Str, node.attr)] |
---|
631 | return _new(_ast.Call, func, args, []) |
---|
632 | |
---|
633 | def visit_Subscript(self, node): |
---|
634 | if not isinstance(node.ctx, _ast.Load) or \ |
---|
635 | not isinstance(node.slice, _ast.Index): |
---|
636 | return ASTTransformer.visit_Subscript(self, node) |
---|
637 | |
---|
638 | func = _new(_ast.Name, '_lookup_item', _ast.Load()) |
---|
639 | args = [ |
---|
640 | self.visit(node.value), |
---|
641 | _new(_ast.Tuple, (self.visit(node.slice.value),), _ast.Load()) |
---|
642 | ] |
---|
643 | return _new(_ast.Call, func, args, []) |
---|