| Index: third_party/logilab/logilab/astroid/test_utils.py
|
| diff --git a/third_party/logilab/logilab/astroid/test_utils.py b/third_party/logilab/logilab/astroid/test_utils.py
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..19bd7b96b6b8cd0b32b2b6288aec3a7ab98d2e76
|
| --- /dev/null
|
| +++ b/third_party/logilab/logilab/astroid/test_utils.py
|
| @@ -0,0 +1,218 @@
|
| +"""Utility functions for test code that uses astroid ASTs as input."""
|
| +import functools
|
| +import sys
|
| +import textwrap
|
| +
|
| +from astroid import nodes
|
| +from astroid import builder
|
| +# The name of the transient function that is used to
|
| +# wrap expressions to be extracted when calling
|
| +# extract_node.
|
| +_TRANSIENT_FUNCTION = '__'
|
| +
|
| +# The comment used to select a statement to be extracted
|
| +# when calling extract_node.
|
| +_STATEMENT_SELECTOR = '#@'
|
| +
|
| +
|
| +def _extract_expressions(node):
|
| + """Find expressions in a call to _TRANSIENT_FUNCTION and extract them.
|
| +
|
| + The function walks the AST recursively to search for expressions that
|
| + are wrapped into a call to _TRANSIENT_FUNCTION. If it finds such an
|
| + expression, it completely removes the function call node from the tree,
|
| + replacing it by the wrapped expression inside the parent.
|
| +
|
| + :param node: An astroid node.
|
| + :type node: astroid.bases.NodeNG
|
| + :yields: The sequence of wrapped expressions on the modified tree
|
| + expression can be found.
|
| + """
|
| + if (isinstance(node, nodes.CallFunc)
|
| + and isinstance(node.func, nodes.Name)
|
| + and node.func.name == _TRANSIENT_FUNCTION):
|
| + real_expr = node.args[0]
|
| + real_expr.parent = node.parent
|
| + # Search for node in all _astng_fields (the fields checked when
|
| + # get_children is called) of its parent. Some of those fields may
|
| + # be lists or tuples, in which case the elements need to be checked.
|
| + # When we find it, replace it by real_expr, so that the AST looks
|
| + # like no call to _TRANSIENT_FUNCTION ever took place.
|
| + for name in node.parent._astroid_fields:
|
| + child = getattr(node.parent, name)
|
| + if isinstance(child, (list, tuple)):
|
| + for idx, compound_child in enumerate(child):
|
| + if compound_child is node:
|
| + child[idx] = real_expr
|
| + elif child is node:
|
| + setattr(node.parent, name, real_expr)
|
| + yield real_expr
|
| + else:
|
| + for child in node.get_children():
|
| + for result in _extract_expressions(child):
|
| + yield result
|
| +
|
| +
|
| +def _find_statement_by_line(node, line):
|
| + """Extracts the statement on a specific line from an AST.
|
| +
|
| + If the line number of node matches line, it will be returned;
|
| + otherwise its children are iterated and the function is called
|
| + recursively.
|
| +
|
| + :param node: An astroid node.
|
| + :type node: astroid.bases.NodeNG
|
| + :param line: The line number of the statement to extract.
|
| + :type line: int
|
| + :returns: The statement on the line, or None if no statement for the line
|
| + can be found.
|
| + :rtype: astroid.bases.NodeNG or None
|
| + """
|
| + if isinstance(node, (nodes.Class, nodes.Function)):
|
| + # This is an inaccuracy in the AST: the nodes that can be
|
| + # decorated do not carry explicit information on which line
|
| + # the actual definition (class/def), but .fromline seems to
|
| + # be close enough.
|
| + node_line = node.fromlineno
|
| + else:
|
| + node_line = node.lineno
|
| +
|
| + if node_line == line:
|
| + return node
|
| +
|
| + for child in node.get_children():
|
| + result = _find_statement_by_line(child, line)
|
| + if result:
|
| + return result
|
| +
|
| + return None
|
| +
|
| +def extract_node(code, module_name=''):
|
| + """Parses some Python code as a module and extracts a designated AST node.
|
| +
|
| + Statements:
|
| + To extract one or more statement nodes, append #@ to the end of the line
|
| +
|
| + Examples:
|
| + >>> def x():
|
| + >>> def y():
|
| + >>> return 1 #@
|
| +
|
| + The return statement will be extracted.
|
| +
|
| + >>> class X(object):
|
| + >>> def meth(self): #@
|
| + >>> pass
|
| +
|
| + The funcion object 'meth' will be extracted.
|
| +
|
| + Expressions:
|
| + To extract arbitrary expressions, surround them with the fake
|
| + function call __(...). After parsing, the surrounded expression
|
| + will be returned and the whole AST (accessible via the returned
|
| + node's parent attribute) will look like the function call was
|
| + never there in the first place.
|
| +
|
| + Examples:
|
| + >>> a = __(1)
|
| +
|
| + The const node will be extracted.
|
| +
|
| + >>> def x(d=__(foo.bar)): pass
|
| +
|
| + The node containing the default argument will be extracted.
|
| +
|
| + >>> def foo(a, b):
|
| + >>> return 0 < __(len(a)) < b
|
| +
|
| + The node containing the function call 'len' will be extracted.
|
| +
|
| + If no statements or expressions are selected, the last toplevel
|
| + statement will be returned.
|
| +
|
| + If the selected statement is a discard statement, (i.e. an expression
|
| + turned into a statement), the wrapped expression is returned instead.
|
| +
|
| + For convenience, singleton lists are unpacked.
|
| +
|
| + :param str code: A piece of Python code that is parsed as
|
| + a module. Will be passed through textwrap.dedent first.
|
| + :param str module_name: The name of the module.
|
| + :returns: The designated node from the parse tree, or a list of nodes.
|
| + :rtype: astroid.bases.NodeNG, or a list of nodes.
|
| + """
|
| + def _extract(node):
|
| + if isinstance(node, nodes.Discard):
|
| + return node.value
|
| + else:
|
| + return node
|
| +
|
| + requested_lines = []
|
| + for idx, line in enumerate(code.splitlines()):
|
| + if line.strip().endswith(_STATEMENT_SELECTOR):
|
| + requested_lines.append(idx + 1)
|
| +
|
| + tree = build_module(code, module_name=module_name)
|
| + extracted = []
|
| + if requested_lines:
|
| + for line in requested_lines:
|
| + extracted.append(_find_statement_by_line(tree, line))
|
| +
|
| + # Modifies the tree.
|
| + extracted.extend(_extract_expressions(tree))
|
| +
|
| + if not extracted:
|
| + extracted.append(tree.body[-1])
|
| +
|
| + extracted = [_extract(node) for node in extracted]
|
| + if len(extracted) == 1:
|
| + return extracted[0]
|
| + else:
|
| + return extracted
|
| +
|
| +
|
| +def build_module(code, module_name='', path=None):
|
| + """Parses a string module with a builder.
|
| + :param code: The code for the module.
|
| + :type code: str
|
| + :param module_name: The name for the module
|
| + :type module_name: str
|
| + :param path: The path for the module
|
| + :type module_name: str
|
| + :returns: The module AST.
|
| + :rtype: astroid.bases.NodeNG
|
| + """
|
| + code = textwrap.dedent(code)
|
| + return builder.AstroidBuilder(None).string_build(code, modname=module_name, path=path)
|
| +
|
| +
|
| +def require_version(minver=None, maxver=None):
|
| + """ Compare version of python interpreter to the given one. Skip the test
|
| + if older.
|
| + """
|
| + def parse(string, default=None):
|
| + string = string or default
|
| + try:
|
| + return tuple(int(v) for v in string.split('.'))
|
| + except ValueError:
|
| + raise ValueError('%s is not a correct version : should be X.Y[.Z].' % version)
|
| +
|
| + def check_require_version(f):
|
| + current = sys.version_info[:3]
|
| + if parse(minver, "0") < current <= parse(maxver, "4"):
|
| + return f
|
| + else:
|
| + str_version = '.'.join(str(v) for v in sys.version_info)
|
| + @functools.wraps(f)
|
| + def new_f(self, *args, **kwargs):
|
| + if minver is not None:
|
| + self.skipTest('Needs Python > %s. Current version is %s.' % (minver, str_version))
|
| + elif maxver is not None:
|
| + self.skipTest('Needs Python <= %s. Current version is %s.' % (maxver, str_version))
|
| + return new_f
|
| +
|
| +
|
| + return check_require_version
|
| +
|
| +def get_name_node(start_from, name, index=0):
|
| + return [n for n in start_from.nodes_of_class(nodes.Name) if n.name == name][index]
|
|
|