diff options
| author | Claudiu Popa <pcmanticore@gmail.com> | 2016-02-11 14:25:36 +0000 |
|---|---|---|
| committer | Claudiu Popa <pcmanticore@gmail.com> | 2016-02-11 14:25:36 +0000 |
| commit | 8aae5f078b5c837ba6829e8042453c12dfe2421f (patch) | |
| tree | 7b4a723c3f4a6e60a227d45726ba76171d8a69ad /astroid/builder.py | |
| parent | 5259a4db2822a1926b66e7b15f059a874bee324d (diff) | |
| download | astroid-git-8aae5f078b5c837ba6829e8042453c12dfe2421f.tar.gz | |
Revert "Try to solve the packaging issues again"
This reverts commit d37b81de4f1e64abc2f222c487785d816ab469ea.
Diffstat (limited to 'astroid/builder.py')
| -rw-r--r-- | astroid/builder.py | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/astroid/builder.py b/astroid/builder.py new file mode 100644 index 00000000..b3a5d0f0 --- /dev/null +++ b/astroid/builder.py @@ -0,0 +1,249 @@ +# copyright 2003-2014 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of astroid. +# +# astroid is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 2.1 of the License, or (at your +# option) any later version. +# +# astroid is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with astroid. If not, see <http://www.gnu.org/licenses/>. +"""The AstroidBuilder makes astroid from living object and / or from _ast + +The builder is not thread safe and can't be used to parse different sources +at the same time. +""" + +import ast +import os +import sys +import textwrap + +from astroid import exceptions +from astroid.interpreter import runtimeabc +from astroid import manager +from astroid import modutils +from astroid.tree import rebuilder +from astroid.tree import treeabc +from astroid import util + +raw_building = util.lazy_import('raw_building') + + +def _parse(string): + return compile(string, "<string>", 'exec', ast.PyCF_ONLY_AST) + + +if sys.version_info >= (3, 0): + # pylint: disable=no-name-in-module; We don't understand flows yet. + from tokenize import detect_encoding + + def open_source_file(filename): + with open(filename, 'rb') as byte_stream: + encoding = detect_encoding(byte_stream.readline)[0] + stream = open(filename, 'r', newline=None, encoding=encoding) + data = stream.read() + return stream, encoding, data + +else: + import re + + _ENCODING_RGX = re.compile(r"\s*#+.*coding[:=]\s*([-\w.]+)") + + def _guess_encoding(string): + """get encoding from a python file as string or return None if not found""" + # check for UTF-8 byte-order mark + if string.startswith('\xef\xbb\xbf'): + return 'UTF-8' + for line in string.split('\n', 2)[:2]: + # check for encoding declaration + match = _ENCODING_RGX.match(line) + if match is not None: + return match.group(1) + + def open_source_file(filename): + """get data for parsing a file""" + stream = open(filename, 'U') + data = stream.read() + encoding = _guess_encoding(data) + return stream, encoding, data + + +MANAGER = manager.AstroidManager() + + +class AstroidBuilder(object): + """Class for building an astroid tree from source code or from a live module. + + The param *manager* specifies the manager class which should be used. + If no manager is given, then the default one will be used. The + param *apply_transforms* determines if the transforms should be + applied after the tree was built from source or from a live object, + by default being True. + """ + + def __init__(self, manager=None, apply_transforms=True): + self._manager = manager or MANAGER + self._apply_transforms = apply_transforms + + def module_build(self, module, modname=None): + """Build an astroid from a living module instance.""" + node = None + path = getattr(module, '__file__', None) + if path is not None: + path_, ext = os.path.splitext(modutils._path_from_filename(path)) + if ext in ('.py', '.pyc', '.pyo') and os.path.exists(path_ + '.py'): + node = self.file_build(path_ + '.py', modname) + if node is None: + # this is a built-in module + # get a partial representation by introspection + node = raw_building.ast_from_object(module, name=modname) + # FIXME + node.source_file = path + if self._apply_transforms: + # We have to handle transformation by ourselves since the + # rebuilder isn't called for builtin nodes + node = self._manager.visit_transforms(node) + return node + + def file_build(self, path, modname=None): + """Build astroid from a source code file (i.e. from an ast) + + *path* is expected to be a python source file + """ + try: + stream, encoding, data = open_source_file(path) + except IOError as exc: + util.reraise(exceptions.AstroidBuildingError( + 'Unable to load file {path}:\n{error}', + modname=modname, path=path, error=exc)) + except (SyntaxError, LookupError) as exc: + util.reraise(exceptions.AstroidSyntaxError( + 'Python 3 encoding specification error or unknown encoding:\n' + '{error}', modname=modname, path=path, error=exc)) + except UnicodeError: # wrong encoding + # detect_encoding returns utf-8 if no encoding specified + util.reraise(exceptions.AstroidBuildingError( + 'Wrong ({encoding}) or no encoding specified for {filename}.', + encoding=encoding, filename=path)) + with stream: + # get module name if necessary + if modname is None: + try: + modname = '.'.join(modutils.modpath_from_file(path)) + except ImportError: + modname = os.path.splitext(os.path.basename(path))[0] + # build astroid representation + module = self._data_build(data, modname, path) + return self._post_build(module, encoding) + + def string_build(self, data, modname='', path=None): + """Build astroid from source code string.""" + module = self._data_build(data, modname, path) + module.source_code = data.encode('utf-8') + return self._post_build(module, 'utf-8') + + def _post_build(self, module, encoding): + """Handles encoding and delayed nodes after a module has been built""" + module.file_encoding = encoding + self._manager.cache_module(module) + delayed_assignments(module) + + # Visit the transforms + if self._apply_transforms: + module = self._manager.visit_transforms(module) + return module + + def _data_build(self, data, modname, path): + """Build tree node from data and add some informations""" + try: + node = _parse(data + '\n') + except (TypeError, ValueError, SyntaxError) as exc: + util.reraise(exceptions.AstroidSyntaxError( + 'Parsing Python code failed:\n{error}', + source=data, modname=modname, path=path, error=exc)) + if path is not None: + node_file = os.path.abspath(path) + else: + node_file = '<?>' + if modname.endswith('.__init__'): + modname = modname[:-9] + package = True + else: + package = path and path.find('__init__.py') > -1 or False + builder = rebuilder.TreeRebuilder() + module = builder.visit_module(node, modname, node_file, package) + return module + + +def delayed_assignments(root): + '''This function modifies nodes according to AssignAttr nodes. + + It traverses the entire AST, and when it encounters an AssignAttr + node it modifies the instance_attrs or external_attrs of the node + respresenting that object. Because it uses inference functions + that in turn depend on instance_attrs and external_attrs, calling + it a tree that already have instance_attrs and external_attrs set + may crash or fail to modify those variables correctly. + + Args: + root (node_classes.NodeNG): The root of the AST that + delayed_assignments() is searching for assignments. + + ''' + stack = [root] + while stack: + node = stack.pop() + stack.extend(node.get_children()) + if isinstance(node, treeabc.AssignAttr): + frame = node.frame() + try: + # Here, node.expr.infer() will return either the node + # being assigned to itself, for Module, ClassDef, + # FunctionDef, or Lambda nodes, or an Instance object + # corresponding to a ClassDef node. + for inferred in node.expr.infer(): + if isinstance(inferred, runtimeabc.Instance): + values = inferred._proxied.instance_attrs[node.attrname] + elif isinstance(inferred, treeabc.Lambda): + values = inferred.instance_attrs[node.attrname] + elif isinstance(inferred, (treeabc.Module, treeabc.ClassDef)): + values = inferred.external_attrs[node.attrname] + else: + continue + if node in values: + continue + else: + # I have no idea why there's a special case + # for __init__ that changes the order of the + # attributes or what that order means. + if (values and frame.name == '__init__' and not + values[0].frame().name == '__init__'): + values.insert(0, node) + else: + values.append(node) + except (exceptions.InferenceError, exceptions.AstroidBuildingError): + pass + + +def parse(code, module_name='', path=None, apply_transforms=True): + """Parses a source string in order to obtain an astroid AST from it + + :param str code: The code for the module. + :param str module_name: The name for the module, if any + :param str path: The path for the module + :param bool apply_transforms: + Apply the transforms for the give code. Use it if you + don't want the default transforms to be applied. + """ + code = textwrap.dedent(code) + builder = AstroidBuilder(manager=MANAGER, + apply_transforms=apply_transforms) + return builder.string_build(code, modname=module_name, path=path) |
