summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorGeorg Brandl <georg@python.org>2008-06-10 07:45:28 (GMT)
committerGeorg Brandl <georg@python.org>2008-06-10 07:45:28 (GMT)
commit8509db5a21abc43086a0b9f6727a89f25ac7d2eb (patch)
treeadce5bbb0264cb1c5ca6ac5f120c503b708ff32b /Lib
parentdf7036d2fac9cb6b6f79bed2b2e0fbe9166f2e5f (diff)
downloadcpython-8509db5a21abc43086a0b9f6727a89f25ac7d2eb.zip
cpython-8509db5a21abc43086a0b9f6727a89f25ac7d2eb.tar.gz
cpython-8509db5a21abc43086a0b9f6727a89f25ac7d2eb.tar.bz2
Add the "ast" module, containing helpers to ease use of the "_ast" classes.
Diffstat (limited to 'Lib')
-rw-r--r--Lib/ast.py300
-rw-r--r--Lib/test/test_ast.py109
2 files changed, 400 insertions, 9 deletions
diff --git a/Lib/ast.py b/Lib/ast.py
new file mode 100644
index 0000000..cc4a4b8
--- /dev/null
+++ b/Lib/ast.py
@@ -0,0 +1,300 @@
+# -*- coding: utf-8 -*-
+"""
+ ast
+ ~~~
+
+ The `ast` module helps Python applications to process trees of the Python
+ abstract syntax grammar. The abstract syntax itself might change with
+ each Python release; this module helps to find out programmatically what
+ the current grammar looks like and allows modifications of it.
+
+ An abstract syntax tree can be generated by passing `ast.PyCF_ONLY_AST` as
+ a flag to the `compile()` builtin function or by using the `parse()`
+ function from this module. The result will be a tree of objects whose
+ classes all inherit from `ast.AST`.
+
+ A modified abstract syntax tree can be compiled into a Python code object
+ using the built-in `compile()` function.
+
+ Additionally various helper functions are provided that make working with
+ the trees simpler. The main intention of the helper functions and this
+ module in general is to provide an easy to use interface for libraries
+ that work tightly with the python syntax (template engines for example).
+
+
+ :copyright: Copyright 2008 by Armin Ronacher.
+ :license: Python License.
+"""
+from _ast import *
+
+
+def parse(expr, filename='<unknown>', mode='exec'):
+ """
+ Parse an expression into an AST node.
+ Equivalent to compile(expr, filename, mode, PyCF_ONLY_AST).
+ """
+ return compile(expr, filename, mode, PyCF_ONLY_AST)
+
+
+def literal_eval(node_or_string):
+ """
+ Safely evaluate an expression node or a string containing a Python
+ expression. The string or node provided may only consist of the following
+ Python literal structures: strings, numbers, tuples, lists, dicts, booleans,
+ and None.
+ """
+ _safe_names = {'None': None, 'True': True, 'False': False}
+ if isinstance(node_or_string, basestring):
+ node_or_string = parse(node_or_string, mode='eval')
+ if isinstance(node_or_string, Expression):
+ node_or_string = node_or_string.body
+ def _convert(node):
+ if isinstance(node, Str):
+ return node.s
+ elif isinstance(node, Num):
+ return node.n
+ elif isinstance(node, Tuple):
+ return tuple(map(_convert, node.elts))
+ elif isinstance(node, List):
+ return list(map(_convert, node.elts))
+ elif isinstance(node, Dict):
+ return dict((_convert(k), _convert(v)) for k, v
+ in zip(node.keys, node.values))
+ elif isinstance(node, Name):
+ if node.id in _safe_names:
+ return _safe_names[node.id]
+ raise ValueError('malformed string')
+ return _convert(node_or_string)
+
+
+def dump(node, annotate_fields=True, include_attributes=False):
+ """
+ Return a formatted dump of the tree in *node*. This is mainly useful for
+ debugging purposes. The returned string will show the names and the values
+ for fields. This makes the code impossible to evaluate, so if evaluation is
+ wanted *annotate_fields* must be set to False. Attributes such as line
+ numbers and column offsets are dumped by default. If this is wanted,
+ *include_attributes* can be set to True.
+ """
+ def _format(node):
+ if isinstance(node, AST):
+ fields = [(a, _format(b)) for a, b in iter_fields(node)]
+ rv = '%s(%s' % (node.__class__.__name__, ', '.join(
+ ('%s=%s' % field for field in fields)
+ if annotate_fields else
+ (b for a, b in fields)
+ ))
+ if include_attributes and node._attributes:
+ rv += fields and ', ' or ' '
+ rv += ', '.join('%s=%s' % (a, _format(getattr(node, a)))
+ for a in node._attributes)
+ return rv + ')'
+ elif isinstance(node, list):
+ return '[%s]' % ', '.join(_format(x) for x in node)
+ return repr(node)
+ if not isinstance(node, AST):
+ raise TypeError('expected AST, got %r' % node.__class__.__name__)
+ return _format(node)
+
+
+def copy_location(new_node, old_node):
+ """
+ Copy source location (`lineno` and `col_offset` attributes) from
+ *old_node* to *new_node* if possible, and return *new_node*.
+ """
+ for attr in 'lineno', 'col_offset':
+ if attr in old_node._attributes and attr in new_node._attributes \
+ and hasattr(old_node, attr):
+ setattr(new_node, attr, getattr(old_node, attr))
+ return new_node
+
+
+def fix_missing_locations(node):
+ """
+ When you compile a node tree with compile(), the compiler expects lineno and
+ col_offset attributes for every node that supports them. This is rather
+ tedious to fill in for generated nodes, so this helper adds these attributes
+ recursively where not already set, by setting them to the values of the
+ parent node. It works recursively starting at *node*.
+ """
+ def _fix(node, lineno, col_offset):
+ if 'lineno' in node._attributes:
+ if not hasattr(node, 'lineno'):
+ node.lineno = lineno
+ else:
+ lineno = node.lineno
+ if 'col_offset' in node._attributes:
+ if not hasattr(node, 'col_offset'):
+ node.col_offset = col_offset
+ else:
+ col_offset = node.col_offset
+ for child in iter_child_nodes(node):
+ _fix(child, lineno, col_offset)
+ _fix(node, 1, 0)
+ return node
+
+
+def increment_lineno(node, n=1):
+ """
+ Increment the line number of each node in the tree starting at *node* by *n*.
+ This is useful to "move code" to a different location in a file.
+ """
+ if 'lineno' in node._attributes:
+ node.lineno = getattr(node, 'lineno', 0) + n
+ for child in walk(node):
+ if 'lineno' in child._attributes:
+ child.lineno = getattr(child, 'lineno', 0) + n
+ return node
+
+
+def iter_fields(node):
+ """
+ Yield a tuple of ``(fieldname, value)`` for each field in ``node._fields``
+ that is present on *node*.
+ """
+ for field in node._fields:
+ try:
+ yield field, getattr(node, field)
+ except AttributeError:
+ pass
+
+
+def iter_child_nodes(node):
+ """
+ Yield all direct child nodes of *node*, that is, all fields that are nodes
+ and all items of fields that are lists of nodes.
+ """
+ for name, field in iter_fields(node):
+ if isinstance(field, AST):
+ yield field
+ elif isinstance(field, list):
+ for item in field:
+ if isinstance(item, AST):
+ yield item
+
+
+def get_docstring(node, clean=True):
+ """
+ Return the docstring for the given node or None if no docstring can
+ be found. If the node provided does not have docstrings a TypeError
+ will be raised.
+ """
+ if not isinstance(node, (FunctionDef, ClassDef, Module)):
+ raise TypeError("%r can't have docstrings" % node.__class__.__name__)
+ if node.body and isinstance(node.body[0], Expr) and \
+ isinstance(node.body[0].value, Str):
+ if clean:
+ import inspect
+ return inspect.cleandoc(node.body[0].value.s)
+ return node.body[0].value.s
+
+
+def walk(node):
+ """
+ Recursively yield all child nodes of *node*, in no specified order. This is
+ useful if you only want to modify nodes in place and don't care about the
+ context.
+ """
+ from collections import deque
+ todo = deque([node])
+ while todo:
+ node = todo.popleft()
+ todo.extend(iter_child_nodes(node))
+ yield node
+
+
+class NodeVisitor(object):
+ """
+ A node visitor base class that walks the abstract syntax tree and calls a
+ visitor function for every node found. This function may return a value
+ which is forwarded by the `visit` method.
+
+ This class is meant to be subclassed, with the subclass adding visitor
+ methods.
+
+ Per default the visitor functions for the nodes are ``'visit_'`` +
+ class name of the node. So a `TryFinally` node visit function would
+ be `visit_TryFinally`. This behavior can be changed by overriding
+ the `visit` method. If no visitor function exists for a node
+ (return value `None`) the `generic_visit` visitor is used instead.
+
+ Don't use the `NodeVisitor` if you want to apply changes to nodes during
+ traversing. For this a special visitor exists (`NodeTransformer`) that
+ allows modifications.
+ """
+
+ def visit(self, node):
+ """Visit a node."""
+ method = 'visit_' + node.__class__.__name__
+ visitor = getattr(self, method, self.generic_visit)
+ return visitor(node)
+
+ def generic_visit(self, node):
+ """Called if no explicit visitor function exists for a node."""
+ for field, value in iter_fields(node):
+ if isinstance(value, list):
+ for item in value:
+ if isinstance(item, AST):
+ self.visit(item)
+ elif isinstance(value, AST):
+ self.visit(value)
+
+
+class NodeTransformer(NodeVisitor):
+ """
+ A :class:`NodeVisitor` subclass that walks the abstract syntax tree and
+ allows modification of nodes.
+
+ The `NodeTransformer` will walk the AST and use the return value of the
+ visitor methods to replace or remove the old node. If the return value of
+ the visitor method is ``None``, the node will be removed from its location,
+ otherwise it is replaced with the return value. The return value may be the
+ original node in which case no replacement takes place.
+
+ Here is an example transformer that rewrites all occurrences of name lookups
+ (``foo``) to ``data['foo']``::
+
+ class RewriteName(NodeTransformer):
+
+ def visit_Name(self, node):
+ return copy_location(Subscript(
+ value=Name(id='data', ctx=Load()),
+ slice=Index(value=Str(s=node.id)),
+ ctx=node.ctx
+ ), node)
+
+ Keep in mind that if the node you're operating on has child nodes you must
+ either transform the child nodes yourself or call the :meth:`generic_visit`
+ method for the node first.
+
+ For nodes that were part of a collection of statements (that applies to all
+ statement nodes), the visitor may also return a list of nodes rather than
+ just a single node.
+
+ Usually you use the transformer like this::
+
+ node = YourTransformer().visit(node)
+ """
+
+ def generic_visit(self, node):
+ for field, old_value in iter_fields(node):
+ old_value = getattr(node, field, None)
+ if isinstance(old_value, list):
+ new_values = []
+ for value in old_value:
+ if isinstance(value, AST):
+ value = self.visit(value)
+ if value is None:
+ continue
+ elif not isinstance(value, AST):
+ new_values.extend(value)
+ continue
+ new_values.append(value)
+ old_value[:] = new_values
+ elif isinstance(old_value, AST):
+ new_node = self.visit(old_value)
+ if new_node is None:
+ delattr(node, field)
+ else:
+ setattr(node, field, new_node)
+ return node
diff --git a/Lib/test/test_ast.py b/Lib/test/test_ast.py
index 9d2bd66..00a5aae 100644
--- a/Lib/test/test_ast.py
+++ b/Lib/test/test_ast.py
@@ -1,6 +1,6 @@
import sys, itertools, unittest
from test import test_support
-import _ast
+import ast
def to_tuple(t):
if t is None or isinstance(t, (basestring, int, long, complex)):
@@ -123,9 +123,9 @@ eval_tests = [
class AST_Tests(unittest.TestCase):
def _assert_order(self, ast_node, parent_pos):
- if not isinstance(ast_node, _ast.AST) or ast_node._fields is None:
+ if not isinstance(ast_node, ast.AST) or ast_node._fields is None:
return
- if isinstance(ast_node, (_ast.expr, _ast.stmt, _ast.excepthandler)):
+ if isinstance(ast_node, (ast.expr, ast.stmt, ast.excepthandler)):
node_pos = (ast_node.lineno, ast_node.col_offset)
self.assert_(node_pos >= parent_pos)
parent_pos = (ast_node.lineno, ast_node.col_offset)
@@ -142,29 +142,29 @@ class AST_Tests(unittest.TestCase):
(single_tests, single_results, "single"),
(eval_tests, eval_results, "eval")):
for i, o in itertools.izip(input, output):
- ast_tree = compile(i, "?", kind, _ast.PyCF_ONLY_AST)
+ ast_tree = compile(i, "?", kind, ast.PyCF_ONLY_AST)
self.assertEquals(to_tuple(ast_tree), o)
self._assert_order(ast_tree, (0, 0))
def test_nodeclasses(self):
- x = _ast.BinOp(1, 2, 3, lineno=0)
+ x = ast.BinOp(1, 2, 3, lineno=0)
self.assertEquals(x.left, 1)
self.assertEquals(x.op, 2)
self.assertEquals(x.right, 3)
self.assertEquals(x.lineno, 0)
# node raises exception when not given enough arguments
- self.assertRaises(TypeError, _ast.BinOp, 1, 2)
+ self.assertRaises(TypeError, ast.BinOp, 1, 2)
# can set attributes through kwargs too
- x = _ast.BinOp(left=1, op=2, right=3, lineno=0)
+ x = ast.BinOp(left=1, op=2, right=3, lineno=0)
self.assertEquals(x.left, 1)
self.assertEquals(x.op, 2)
self.assertEquals(x.right, 3)
self.assertEquals(x.lineno, 0)
# this used to fail because Sub._fields was None
- x = _ast.Sub()
+ x = ast.Sub()
def test_pickling(self):
import pickle
@@ -181,8 +181,99 @@ class AST_Tests(unittest.TestCase):
ast2 = mod.loads(mod.dumps(ast, protocol))
self.assertEquals(to_tuple(ast2), to_tuple(ast))
+
+class ASTHelpers_Test(unittest.TestCase):
+
+ def test_parse(self):
+ a = ast.parse('foo(1 + 1)')
+ b = compile('foo(1 + 1)', '<unknown>', 'exec', ast.PyCF_ONLY_AST)
+ self.assertEqual(ast.dump(a), ast.dump(b))
+
+ def test_dump(self):
+ node = ast.parse('spam(eggs, "and cheese")')
+ self.assertEqual(ast.dump(node),
+ "Module(body=[Expr(value=Call(func=Name(id='spam', ctx=Load()), "
+ "args=[Name(id='eggs', ctx=Load()), Str(s='and cheese')], "
+ "keywords=[], starargs=None, kwargs=None))])"
+ )
+ self.assertEqual(ast.dump(node, annotate_fields=False),
+ "Module([Expr(Call(Name('spam', Load()), [Name('eggs', Load()), "
+ "Str('and cheese')], [], None, None))])"
+ )
+ self.assertEqual(ast.dump(node, include_attributes=True),
+ "Module(body=[Expr(value=Call(func=Name(id='spam', ctx=Load(), "
+ "lineno=1, col_offset=0), args=[Name(id='eggs', ctx=Load(), "
+ "lineno=1, col_offset=5), Str(s='and cheese', lineno=1, "
+ "col_offset=11)], keywords=[], starargs=None, kwargs=None, "
+ "lineno=1, col_offset=0), lineno=1, col_offset=0)])"
+ )
+
+ def test_copy_location(self):
+ src = ast.parse('1 + 1', mode='eval')
+ src.body.right = ast.copy_location(ast.Num(2), src.body.right)
+ self.assertEqual(ast.dump(src, include_attributes=True),
+ 'Expression(body=BinOp(left=Num(n=1, lineno=1, col_offset=0), '
+ 'op=Add(), right=Num(n=2, lineno=1, col_offset=4), lineno=1, '
+ 'col_offset=0))'
+ )
+
+ def test_fix_missing_locations(self):
+ src = ast.parse('write("spam")')
+ src.body.append(ast.Expr(ast.Call(ast.Name('spam', ast.Load()),
+ [ast.Str('eggs')], [], None, None)))
+ self.assertEqual(src, ast.fix_missing_locations(src))
+ self.assertEqual(ast.dump(src, include_attributes=True),
+ "Module(body=[Expr(value=Call(func=Name(id='write', ctx=Load(), "
+ "lineno=1, col_offset=0), args=[Str(s='spam', lineno=1, "
+ "col_offset=6)], keywords=[], starargs=None, kwargs=None, "
+ "lineno=1, col_offset=0), lineno=1, col_offset=0), "
+ "Expr(value=Call(func=Name(id='spam', ctx=Load(), lineno=1, "
+ "col_offset=0), args=[Str(s='eggs', lineno=1, col_offset=0)], "
+ "keywords=[], starargs=None, kwargs=None, lineno=1, "
+ "col_offset=0), lineno=1, col_offset=0)])"
+ )
+
+ def test_increment_lineno(self):
+ src = ast.parse('1 + 1', mode='eval')
+ self.assertEqual(ast.increment_lineno(src, n=3), src)
+ self.assertEqual(ast.dump(src, include_attributes=True),
+ 'Expression(body=BinOp(left=Num(n=1, lineno=4, col_offset=0), '
+ 'op=Add(), right=Num(n=1, lineno=4, col_offset=4), lineno=4, '
+ 'col_offset=0))'
+ )
+
+ def test_iter_fields(self):
+ node = ast.parse('foo()', mode='eval')
+ d = dict(ast.iter_fields(node.body))
+ self.assertEqual(d.pop('func').id, 'foo')
+ self.assertEqual(d, {'keywords': [], 'kwargs': None,
+ 'args': [], 'starargs': None})
+
+ def test_iter_child_nodes(self):
+ node = ast.parse("spam(23, 42, eggs='leek')", mode='eval')
+ self.assertEqual(len(list(ast.iter_child_nodes(node.body))), 4)
+ iterator = ast.iter_child_nodes(node.body)
+ self.assertEqual(next(iterator).id, 'spam')
+ self.assertEqual(next(iterator).n, 23)
+ self.assertEqual(next(iterator).n, 42)
+ self.assertEqual(ast.dump(next(iterator)),
+ "keyword(arg='eggs', value=Str(s='leek'))"
+ )
+
+ def test_get_docstring(self):
+ node = ast.parse('def foo():\n """line one\n line two"""')
+ self.assertEqual(ast.get_docstring(node.body[0]),
+ 'line one\nline two')
+
+ def test_literal_eval(self):
+ self.assertEqual(ast.literal_eval('[1, 2, 3]'), [1, 2, 3])
+ self.assertEqual(ast.literal_eval('{"foo": 42}'), {"foo": 42})
+ self.assertEqual(ast.literal_eval('(True, False, None)'), (True, False, None))
+ self.assertRaises(ValueError, ast.literal_eval, 'foo()')
+
+
def test_main():
- test_support.run_unittest(AST_Tests)
+ test_support.run_unittest(AST_Tests, ASTHelpers_Test)
def main():
if __name__ != '__main__':