/resources/python/logilab/astng/builder.py
Python | 231 lines | 180 code | 9 blank | 42 comment | 19 complexity | 4e7c81841413f021201f010433d6d013 MD5 | raw file
1# copyright 2003-2010 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
2# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
3# copyright 2003-2010 Sylvain Thenault, all rights reserved.
4# contact mailto:thenault@gmail.com
5#
6# This file is part of logilab-astng.
7#
8# logilab-astng is free software: you can redistribute it and/or modify it
9# under the terms of the GNU Lesser General Public License as published by the
10# Free Software Foundation, either version 2.1 of the License, or (at your
11# option) any later version.
12#
13# logilab-astng is distributed in the hope that it will be useful, but
14# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
15# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
16# for more details.
17#
18# You should have received a copy of the GNU Lesser General Public License along
19# with logilab-astng. If not, see <http://www.gnu.org/licenses/>.
20"""The ASTNGBuilder makes astng from living object and / or from compiler.ast
21
22With python >= 2.5, the internal _ast module is used instead
23
24The builder is not thread safe and can't be used to parse different sources
25at the same time.
26"""
27
28__docformat__ = "restructuredtext en"
29
30import sys, re
31from os.path import splitext, basename, dirname, exists, abspath
32
33from logilab.common.modutils import modpath_from_file
34
35from logilab.astng.exceptions import ASTNGBuildingException, InferenceError
36from logilab.astng.raw_building import InspectBuilder
37from logilab.astng.rebuilder import TreeRebuilder
38from logilab.astng.manager import ASTNGManager
39from logilab.astng.bases import YES, Instance
40
41from _ast import PyCF_ONLY_AST
42def parse(string):
43 return compile(string, "<string>", 'exec', PyCF_ONLY_AST)
44
45if sys.version_info >= (3, 0):
46 from tokenize import detect_encoding
47
48 def open_source_file(filename):
49 byte_stream = open(filename, 'bU')
50 encoding = detect_encoding(byte_stream.readline)[0]
51 stream = open(filename, 'U', encoding=encoding)
52 try:
53 data = stream.read()
54 except UnicodeError, uex: # wrong encodingg
55 # detect_encoding returns utf-8 if no encoding specified
56 msg = 'Wrong (%s) or no encoding specified' % encoding
57 raise ASTNGBuildingException(msg)
58 return stream, encoding, data
59
60else:
61 import re
62
63 _ENCODING_RGX = re.compile("[^#]*#*.*coding[:=]\s*([^\s]+)")
64
65 def _guess_encoding(string):
66 """get encoding from a python file as string or return None if not found
67 """
68 # check for UTF-8 byte-order mark
69 if string.startswith('\xef\xbb\xbf'):
70 return 'UTF-8'
71 for line in string.split('\n', 2)[:2]:
72 # check for encoding declaration
73 match = _ENCODING_RGX.match(line)
74 if match is not None:
75 return match.group(1)
76
77 def open_source_file(filename):
78 """get data for parsing a file"""
79 stream = open(filename, 'U')
80 data = stream.read()
81 encoding = _guess_encoding(data)
82 return stream, encoding, data
83
84# ast NG builder ##############################################################
85
86MANAGER = ASTNGManager()
87
88class ASTNGBuilder(InspectBuilder):
89 """provide astng building methods"""
90 rebuilder = TreeRebuilder()
91
92 def __init__(self, manager=None):
93 self._manager = manager or MANAGER
94
95 def module_build(self, module, modname=None):
96 """build an astng from a living module instance
97 """
98 node = None
99 path = getattr(module, '__file__', None)
100 if path is not None:
101 path_, ext = splitext(module.__file__)
102 if ext in ('.py', '.pyc', '.pyo') and exists(path_ + '.py'):
103 node = self.file_build(path_ + '.py', modname)
104 if node is None:
105 # this is a built-in module
106 # get a partial representation by introspection
107 node = self.inspect_build(module, modname=modname, path=path)
108 return node
109
110 def file_build(self, path, modname=None):
111 """build astng from a source code file (i.e. from an ast)
112
113 path is expected to be a python source file
114 """
115 try:
116 stream, encoding, data = open_source_file(path)
117 except IOError, exc:
118 msg = 'Unable to load file %r (%s)' % (path, exc)
119 raise ASTNGBuildingException(msg)
120 except SyntaxError, exc: # py3k encoding specification error
121 raise ASTNGBuildingException(exc)
122 except LookupError, exc: # unknown encoding
123 raise ASTNGBuildingException(exc)
124 # get module name if necessary, *before modifying sys.path*
125 if modname is None:
126 try:
127 modname = '.'.join(modpath_from_file(path))
128 except ImportError:
129 modname = splitext(basename(path))[0]
130 # build astng representation
131 try:
132 sys.path.insert(0, dirname(path)) # XXX (syt) iirk
133 node = self.string_build(data, modname, path)
134 finally:
135 sys.path.pop(0)
136 node.file_encoding = encoding
137 node.file_stream = stream
138 return node
139
140 def string_build(self, data, modname='', path=None):
141 """build astng from source code string and return rebuilded astng"""
142 module = self._data_build(data, modname, path)
143 if self._manager is not None:
144 self._manager.astng_cache[module.name] = module
145 # post tree building steps after we stored the module in the cache:
146 for from_node in module._from_nodes:
147 self.add_from_names_to_locals(from_node)
148 # handle delayed assattr nodes
149 for delayed in module._delayed_assattr:
150 self.delayed_assattr(delayed)
151 return module
152
153 def _data_build(self, data, modname, path):
154 """build tree node from data and add some informations"""
155 # this method could be wrapped with a pickle/cache function
156 node = parse(data + '\n')
157 if path is not None:
158 node_file = abspath(path)
159 else:
160 node_file = '<?>'
161 if modname.endswith('.__init__'):
162 modname = modname[:-9]
163 package = True
164 else:
165 package = path and path.find('__init__.py') > -1 or False
166 self.rebuilder.init()
167 module = self.rebuilder.visit_module(node, modname, package)
168 module.file = module.path = node_file
169 module._from_nodes = self.rebuilder._from_nodes
170 module._delayed_assattr = self.rebuilder._delayed_assattr
171 return module
172
173 def add_from_names_to_locals(self, node):
174 """store imported names to the locals;
175 resort the locals if coming from a delayed node
176 """
177
178 _key_func = lambda node: node.fromlineno
179 def sort_locals(my_list):
180 my_list.sort(key=_key_func)
181 for (name, asname) in node.names:
182 if name == '*':
183 try:
184 imported = node.root().import_module(node.modname)
185 except ASTNGBuildingException:
186 continue
187 for name in imported.wildcard_import_names():
188 node.parent.set_local(name, node)
189 sort_locals(node.parent.scope().locals[name])
190 else:
191 node.parent.set_local(asname or name, node)
192 sort_locals(node.parent.scope().locals[asname or name])
193
194 def delayed_assattr(self, node):
195 """visit a AssAttr node -> add name to locals, handle members
196 definition
197 """
198 try:
199 frame = node.frame()
200 for infered in node.expr.infer():
201 if infered is YES:
202 continue
203 try:
204 if infered.__class__ is Instance:
205 infered = infered._proxied
206 iattrs = infered.instance_attrs
207 elif isinstance(infered, Instance):
208 # Const, Tuple, ... we may be wrong, may be not, but
209 # anyway we don't want to pollute builtin's namespace
210 continue
211 elif infered.is_function:
212 iattrs = infered.instance_attrs
213 else:
214 iattrs = infered.locals
215 except AttributeError:
216 # XXX log error
217 #import traceback
218 #traceback.print_exc()
219 continue
220 values = iattrs.setdefault(node.attrname, [])
221 if node in values:
222 continue
223 # get assign in __init__ first XXX useful ?
224 if frame.name == '__init__' and values and not \
225 values[0].frame().name == '__init__':
226 values.insert(0, node)
227 else:
228 values.append(node)
229 except InferenceError:
230 pass
231