| 1 | # This program is free software; you can redistribute it and/or modify it under |
|---|
| 2 | # the terms of the GNU General Public License as published by the Free Software |
|---|
| 3 | # Foundation; either version 2 of the License, or (at your option) any later |
|---|
| 4 | # version. |
|---|
| 5 | # |
|---|
| 6 | # This program is distributed in the hope that it will be useful, but WITHOUT |
|---|
| 7 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
|---|
| 8 | # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. |
|---|
| 9 | # |
|---|
| 10 | # You should have received a copy of the GNU General Public License along with |
|---|
| 11 | # this program; if not, write to the Free Software Foundation, Inc., |
|---|
| 12 | # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. |
|---|
| 13 | """The ASTNGBuilder makes astng from living object and / or from compiler.ast |
|---|
| 14 | |
|---|
| 15 | The builder is not thread safe and can't be used to parse different sources |
|---|
| 16 | at the same time. |
|---|
| 17 | |
|---|
| 18 | TODO: |
|---|
| 19 | - more complet representation on inspect build |
|---|
| 20 | (imported modules ? use dis.dis ?) |
|---|
| 21 | |
|---|
| 22 | |
|---|
| 23 | :author: Sylvain Thenault |
|---|
| 24 | :copyright: 2003-2007 LOGILAB S.A. (Paris, FRANCE) |
|---|
| 25 | :contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org |
|---|
| 26 | :copyright: 2003-2007 Sylvain Thenault |
|---|
| 27 | :contact: mailto:thenault@gmail.com |
|---|
| 28 | """ |
|---|
| 29 | |
|---|
| 30 | __docformat__ = "restructuredtext en" |
|---|
| 31 | |
|---|
| 32 | import sys |
|---|
| 33 | from os.path import splitext, basename, dirname, exists, abspath |
|---|
| 34 | from parser import ParserError |
|---|
| 35 | from compiler import parse |
|---|
| 36 | from inspect import isfunction, ismethod, ismethoddescriptor, isclass, \ |
|---|
| 37 | isbuiltin |
|---|
| 38 | from inspect import isdatadescriptor |
|---|
| 39 | |
|---|
| 40 | from logilab.common.fileutils import norm_read |
|---|
| 41 | from logilab.common.modutils import modpath_from_file |
|---|
| 42 | |
|---|
| 43 | from logilab.astng import nodes, YES, Instance |
|---|
| 44 | from logilab.astng.utils import ASTWalker |
|---|
| 45 | from logilab.astng._exceptions import ASTNGBuildingException, InferenceError |
|---|
| 46 | from logilab.astng.raw_building import * |
|---|
| 47 | from logilab.astng.astutils import cvrtr |
|---|
| 48 | |
|---|
| 49 | import token |
|---|
| 50 | from compiler import transformer, consts |
|---|
| 51 | from types import TupleType |
|---|
| 52 | |
|---|
| 53 | def fromto_lineno(asttuple): |
|---|
| 54 | """return the minimum and maximum line number of the given ast tuple""" |
|---|
| 55 | return from_lineno(asttuple), to_lineno(asttuple) |
|---|
| 56 | def from_lineno(asttuple): |
|---|
| 57 | """return the minimum line number of the given ast tuple""" |
|---|
| 58 | if type(asttuple[1]) is TupleType: |
|---|
| 59 | return from_lineno(asttuple[1]) |
|---|
| 60 | return asttuple[2] |
|---|
| 61 | def to_lineno(asttuple): |
|---|
| 62 | """return the maximum line number of the given ast tuple""" |
|---|
| 63 | if type(asttuple[-1]) is TupleType: |
|---|
| 64 | return to_lineno(asttuple[-1]) |
|---|
| 65 | return asttuple[2] |
|---|
| 66 | |
|---|
| 67 | def fix_lineno(node, fromast, toast=None): |
|---|
| 68 | if 'fromlineno' in node.__dict__: |
|---|
| 69 | return node |
|---|
| 70 | #print 'fixing', id(node), id(node.__dict__), node.__dict__.keys(), repr(node) |
|---|
| 71 | if isinstance(node, nodes.Stmt): |
|---|
| 72 | node.fromlineno = from_lineno(fromast)#node.nodes[0].fromlineno |
|---|
| 73 | node.tolineno = node.nodes[-1].tolineno |
|---|
| 74 | return node |
|---|
| 75 | if toast is None: |
|---|
| 76 | node.fromlineno, node.tolineno = fromto_lineno(fromast) |
|---|
| 77 | else: |
|---|
| 78 | node.fromlineno, node.tolineno = from_lineno(fromast), to_lineno(toast) |
|---|
| 79 | #print 'fixed', id(node) |
|---|
| 80 | return node |
|---|
| 81 | |
|---|
| 82 | BaseTransformer = transformer.Transformer |
|---|
| 83 | |
|---|
| 84 | COORD_MAP = { |
|---|
| 85 | # if: test ':' suite ('elif' test ':' suite)* ['else' ':' suite] |
|---|
| 86 | 'if': (0, 0), |
|---|
| 87 | # 'while' test ':' suite ['else' ':' suite] |
|---|
| 88 | 'while': (0, 1), |
|---|
| 89 | # 'for' exprlist 'in' exprlist ':' suite ['else' ':' suite] |
|---|
| 90 | 'for': (0, 3), |
|---|
| 91 | # 'try' ':' suite (except_clause ':' suite)+ ['else' ':' suite] |
|---|
| 92 | 'try': (0, 0), |
|---|
| 93 | # | 'try' ':' suite 'finally' ':' suite |
|---|
| 94 | |
|---|
| 95 | } |
|---|
| 96 | |
|---|
| 97 | def fixlineno_wrap(function, stype): |
|---|
| 98 | def fixlineno_wrapper(self, nodelist): |
|---|
| 99 | node = function(self, nodelist) |
|---|
| 100 | idx1, idx2 = COORD_MAP.get(stype, (0, -1)) |
|---|
| 101 | return fix_lineno(node, nodelist[idx1], nodelist[idx2]) |
|---|
| 102 | return fixlineno_wrapper |
|---|
| 103 | nodes.Module.fromlineno = 0 |
|---|
| 104 | nodes.Module.tolineno = 0 |
|---|
| 105 | class ASTNGTransformer(BaseTransformer): |
|---|
| 106 | """ovverides transformer for a better source line number handling""" |
|---|
| 107 | def com_NEWLINE(self, *args): |
|---|
| 108 | # A ';' at the end of a line can make a NEWLINE token appear |
|---|
| 109 | # here, Render it harmless. (genc discards ('discard', |
|---|
| 110 | # ('const', xxxx)) Nodes) |
|---|
| 111 | lineno = args[0][1] |
|---|
| 112 | # don't put fromlineno/tolineno on Const None to mark it as dynamically |
|---|
| 113 | # added, without "physical" reference in the source |
|---|
| 114 | n = nodes.Discard(nodes.Const(None)) |
|---|
| 115 | n.fromlineno = n.tolineno = lineno |
|---|
| 116 | return n |
|---|
| 117 | def com_node(self, node): |
|---|
| 118 | res = self._dispatch[node[0]](node[1:]) |
|---|
| 119 | return fix_lineno(res, node) |
|---|
| 120 | def com_assign(self, node, assigning): |
|---|
| 121 | res = BaseTransformer.com_assign(self, node, assigning) |
|---|
| 122 | return fix_lineno(res, node) |
|---|
| 123 | def com_apply_trailer(self, primaryNode, nodelist): |
|---|
| 124 | node = BaseTransformer.com_apply_trailer(self, primaryNode, nodelist) |
|---|
| 125 | return fix_lineno(node, nodelist) |
|---|
| 126 | |
|---|
| 127 | ## def atom(self, nodelist): |
|---|
| 128 | ## node = BaseTransformer.atom(self, nodelist) |
|---|
| 129 | ## return fix_lineno(node, nodelist[0], nodelist[-1]) |
|---|
| 130 | |
|---|
| 131 | def funcdef(self, nodelist): |
|---|
| 132 | node = BaseTransformer.funcdef(self, nodelist) |
|---|
| 133 | # XXX decorators |
|---|
| 134 | return fix_lineno(node, nodelist[-5], nodelist[-3]) |
|---|
| 135 | def classdef(self, nodelist): |
|---|
| 136 | node = BaseTransformer.classdef(self, nodelist) |
|---|
| 137 | return fix_lineno(node, nodelist[0], nodelist[-2]) |
|---|
| 138 | |
|---|
| 139 | # wrap *_stmt methods |
|---|
| 140 | for name in dir(BaseTransformer): |
|---|
| 141 | if name.endswith('_stmt') and not (name in ('com_stmt', |
|---|
| 142 | 'com_append_stmt') |
|---|
| 143 | or name in ASTNGTransformer.__dict__): |
|---|
| 144 | setattr(BaseTransformer, name, |
|---|
| 145 | fixlineno_wrap(getattr(BaseTransformer, name), name[:-5])) |
|---|
| 146 | |
|---|
| 147 | transformer.Transformer = ASTNGTransformer |
|---|
| 148 | |
|---|
| 149 | # ast NG builder ############################################################## |
|---|
| 150 | |
|---|
| 151 | class ASTNGBuilder: |
|---|
| 152 | """provide astng building methods |
|---|
| 153 | """ |
|---|
| 154 | |
|---|
| 155 | def __init__(self, manager=None): |
|---|
| 156 | if manager is None: |
|---|
| 157 | from logilab.astng import MANAGER as manager |
|---|
| 158 | self._manager = manager |
|---|
| 159 | self._module = None |
|---|
| 160 | self._file = None |
|---|
| 161 | self._done = None |
|---|
| 162 | self._stack, self._par_stack = None, None |
|---|
| 163 | self._metaclass = None |
|---|
| 164 | self._walker = ASTWalker(self) |
|---|
| 165 | self._dyn_modname_map = {'gtk': 'gtk._gtk'} |
|---|
| 166 | self._delayed = [] |
|---|
| 167 | |
|---|
| 168 | def module_build(self, module, modname=None): |
|---|
| 169 | """build an astng from a living module instance |
|---|
| 170 | """ |
|---|
| 171 | node = None |
|---|
| 172 | self._module = module |
|---|
| 173 | path = getattr(module, '__file__', None) |
|---|
| 174 | if path is not None: |
|---|
| 175 | path_, ext = splitext(module.__file__) |
|---|
| 176 | if ext in ('.py', '.pyc', '.pyo') and exists(path_ + '.py'): |
|---|
| 177 | node = self.file_build(path_ + '.py', modname) |
|---|
| 178 | if node is None: |
|---|
| 179 | # this is a built-in module |
|---|
| 180 | # get a partial representation by introspection |
|---|
| 181 | node = self.inspect_build(module, modname=modname, path=path) |
|---|
| 182 | return node |
|---|
| 183 | |
|---|
| 184 | def inspect_build(self, module, modname=None, path=None): |
|---|
| 185 | """build astng from a living module (i.e. using inspect) |
|---|
| 186 | this is used when there is no python source code available (either |
|---|
| 187 | because it's a built-in module or because the .py is not available) |
|---|
| 188 | """ |
|---|
| 189 | self._module = module |
|---|
| 190 | node = build_module(modname or module.__name__, module.__doc__) |
|---|
| 191 | node.file = node.path = path and abspath(path) or path |
|---|
| 192 | if self._manager is not None: |
|---|
| 193 | self._manager._cache[node.file] = self._manager._cache[node.name] = node |
|---|
| 194 | node.package = hasattr(module, '__path__') |
|---|
| 195 | attach___dict__(node) |
|---|
| 196 | self._done = {} |
|---|
| 197 | self.object_build(node, module) |
|---|
| 198 | return node |
|---|
| 199 | |
|---|
| 200 | def file_build(self, path, modname=None): |
|---|
| 201 | """build astng from a source code file (i.e. from an ast) |
|---|
| 202 | |
|---|
| 203 | path is expected to be a python source file |
|---|
| 204 | """ |
|---|
| 205 | try: |
|---|
| 206 | data = norm_read(path) |
|---|
| 207 | except IOError, ex: |
|---|
| 208 | msg = 'Unable to load file %r (%s)' % (path, ex) |
|---|
| 209 | raise ASTNGBuildingException(msg) |
|---|
| 210 | self._file = path |
|---|
| 211 | # get module name if necessary, *before modifying sys.path* |
|---|
| 212 | if modname is None: |
|---|
| 213 | try: |
|---|
| 214 | modname = '.'.join(modpath_from_file(path)) |
|---|
| 215 | except ImportError: |
|---|
| 216 | modname = splitext(basename(path))[0] |
|---|
| 217 | # build astng representation |
|---|
| 218 | try: |
|---|
| 219 | sys.path.insert(0, dirname(path)) |
|---|
| 220 | node = self.string_build(data, modname, path) |
|---|
| 221 | node.file = abspath(path) |
|---|
| 222 | finally: |
|---|
| 223 | self._file = None |
|---|
| 224 | sys.path.pop(0) |
|---|
| 225 | |
|---|
| 226 | return node |
|---|
| 227 | |
|---|
| 228 | def string_build(self, data, modname='', path=None): |
|---|
| 229 | """build astng from a source code stream (i.e. from an ast)""" |
|---|
| 230 | return self.ast_build(parse(data + '\n'), modname, path) |
|---|
| 231 | |
|---|
| 232 | def ast_build(self, node, modname=None, path=None): |
|---|
| 233 | """recurse on the ast (soon ng) to add some arguments et method |
|---|
| 234 | """ |
|---|
| 235 | if path is not None: |
|---|
| 236 | node.file = node.path = abspath(path) |
|---|
| 237 | else: |
|---|
| 238 | node.file = node.path = '<?>' |
|---|
| 239 | if modname.endswith('.__init__'): |
|---|
| 240 | modname = modname[:-9] |
|---|
| 241 | node.package = True |
|---|
| 242 | else: |
|---|
| 243 | node.package = path and path.find('__init__.py') > -1 or False |
|---|
| 244 | node.name = modname |
|---|
| 245 | node.pure_python = True |
|---|
| 246 | if self._manager is not None: |
|---|
| 247 | self._manager._cache[node.file] = node |
|---|
| 248 | if self._file: |
|---|
| 249 | self._manager._cache[abspath(self._file)] = node |
|---|
| 250 | self._walker.walk(node) |
|---|
| 251 | while self._delayed: |
|---|
| 252 | dnode = self._delayed.pop(0) |
|---|
| 253 | getattr(self, 'delayed_visit_%s' % dnode.__class__.__name__.lower())(dnode) |
|---|
| 254 | return node |
|---|
| 255 | |
|---|
| 256 | # callbacks to build from an existing compiler.ast tree ################### |
|---|
| 257 | |
|---|
| 258 | def visit_module(self, node): |
|---|
| 259 | """visit a stmt.Module node -> init node and push the corresponding |
|---|
| 260 | object or None on the top of the stack |
|---|
| 261 | """ |
|---|
| 262 | self._stack = [self._module] |
|---|
| 263 | self._par_stack = [node] |
|---|
| 264 | self._metaclass = [''] |
|---|
| 265 | self._global_names = [] |
|---|
| 266 | node.parent = None |
|---|
| 267 | node.globals = node.locals = {} |
|---|
| 268 | for name, value in ( ('__name__', node.name), |
|---|
| 269 | ('__file__', node.path), |
|---|
| 270 | ('__doc__', node.doc) ): |
|---|
| 271 | const = nodes.Const(value) |
|---|
| 272 | const.parent = node |
|---|
| 273 | node.locals[name] = [const] |
|---|
| 274 | attach___dict__(node) |
|---|
| 275 | if node.package: |
|---|
| 276 | # FIXME: List(Const()) |
|---|
| 277 | const = nodes.Const(dirname(node.path)) |
|---|
| 278 | const.parent = node |
|---|
| 279 | node.locals['__path__'] = [const] |
|---|
| 280 | |
|---|
| 281 | |
|---|
| 282 | def leave_module(self, _): |
|---|
| 283 | """leave a stmt.Module node -> pop the last item on the stack and check |
|---|
| 284 | the stack is empty |
|---|
| 285 | """ |
|---|
| 286 | self._stack.pop() |
|---|
| 287 | assert not self._stack, 'Stack is not empty : %s' % self._stack |
|---|
| 288 | self._par_stack.pop() |
|---|
| 289 | assert not self._par_stack, \ |
|---|
| 290 | 'Parent stack is not empty : %s' % self._par_stack |
|---|
| 291 | |
|---|
| 292 | def visit_class(self, node): |
|---|
| 293 | """visit a stmt.Class node -> init node and push the corresponding |
|---|
| 294 | object or None on the top of the stack |
|---|
| 295 | """ |
|---|
| 296 | self.visit_default(node) |
|---|
| 297 | node.instance_attrs = {} |
|---|
| 298 | node.basenames = [b_node.as_string() for b_node in node.bases] |
|---|
| 299 | self._push(node) |
|---|
| 300 | for name, value in ( ('__name__', node.name), |
|---|
| 301 | ('__module__', node.root().name), |
|---|
| 302 | ('__doc__', node.doc) ): |
|---|
| 303 | const = nodes.Const(value) |
|---|
| 304 | const.parent = node |
|---|
| 305 | node.locals[name] = [const] |
|---|
| 306 | attach___dict__(node) |
|---|
| 307 | self._metaclass.append(self._metaclass[-1]) |
|---|
| 308 | |
|---|
| 309 | def leave_class(self, node): |
|---|
| 310 | """leave a stmt.Class node -> pop the last item on the stack |
|---|
| 311 | """ |
|---|
| 312 | self.leave_default(node) |
|---|
| 313 | self._stack.pop() |
|---|
| 314 | metaclass = self._metaclass.pop() |
|---|
| 315 | if not node.bases: |
|---|
| 316 | # no base classes, detect new / style old style according to |
|---|
| 317 | # current scope |
|---|
| 318 | node._newstyle = metaclass == 'type' |
|---|
| 319 | |
|---|
| 320 | def visit_function(self, node): |
|---|
| 321 | """visit a stmt.Function node -> init node and push the corresponding |
|---|
| 322 | object or None on the top of the stack |
|---|
| 323 | """ |
|---|
| 324 | self.visit_default(node) |
|---|
| 325 | self._global_names.append({}) |
|---|
| 326 | node.argnames = list(node.argnames) |
|---|
| 327 | if isinstance(node.parent.frame(), nodes.Class): |
|---|
| 328 | node.type = 'method' |
|---|
| 329 | if node.name == '__new__': |
|---|
| 330 | node.type = 'classmethod' |
|---|
| 331 | self._push(node) |
|---|
| 332 | register_arguments(node, node.argnames) |
|---|
| 333 | |
|---|
| 334 | def leave_function(self, node): |
|---|
| 335 | """leave a stmt.Function node -> pop the last item on the stack |
|---|
| 336 | """ |
|---|
| 337 | self.leave_default(node) |
|---|
| 338 | self._stack.pop() |
|---|
| 339 | self._global_names.pop() |
|---|
| 340 | |
|---|
| 341 | def visit_lambda(self, node): |
|---|
| 342 | """visit a stmt.Lambda node -> init node locals |
|---|
| 343 | """ |
|---|
| 344 | self.visit_default(node) |
|---|
| 345 | node.argnames = list(node.argnames) |
|---|
| 346 | node.locals = {} |
|---|
| 347 | register_arguments(node, node.argnames) |
|---|
| 348 | |
|---|
| 349 | def visit_genexpr(self, node): |
|---|
| 350 | """visit a stmt.GenExpr node -> init node locals |
|---|
| 351 | """ |
|---|
| 352 | self.visit_default(node) |
|---|
| 353 | node.locals = {} |
|---|
| 354 | |
|---|
| 355 | def visit_global(self, node): |
|---|
| 356 | """visit a stmt.Global node -> add declared names to locals |
|---|
| 357 | """ |
|---|
| 358 | self.visit_default(node) |
|---|
| 359 | if not self._global_names: # global at the module level, no effect |
|---|
| 360 | return |
|---|
| 361 | for name in node.names: |
|---|
| 362 | self._global_names[-1].setdefault(name, []).append(node) |
|---|
| 363 | # node.parent.set_local(name, node) |
|---|
| 364 | # module = node.root() |
|---|
| 365 | # if module is not node.frame(): |
|---|
| 366 | # for name in node.names: |
|---|
| 367 | # module.set_local(name, node) |
|---|
| 368 | |
|---|
| 369 | def visit_import(self, node): |
|---|
| 370 | """visit a stmt.Import node -> add imported names to locals |
|---|
| 371 | """ |
|---|
| 372 | self.visit_default(node) |
|---|
| 373 | for (name, asname) in node.names: |
|---|
| 374 | name = asname or name |
|---|
| 375 | node.parent.set_local(name.split('.')[0], node) |
|---|
| 376 | |
|---|
| 377 | def visit_from(self, node): |
|---|
| 378 | """visit a stmt.From node -> add imported names to locals |
|---|
| 379 | """ |
|---|
| 380 | self.visit_default(node) |
|---|
| 381 | # add names imported by the import to locals |
|---|
| 382 | for (name, asname) in node.names: |
|---|
| 383 | if name == '*': |
|---|
| 384 | try: |
|---|
| 385 | imported = node.root().import_module(node.modname) |
|---|
| 386 | except ASTNGBuildingException: |
|---|
| 387 | #import traceback |
|---|
| 388 | #traceback.print_exc() |
|---|
| 389 | continue |
|---|
| 390 | # FIXME: log error |
|---|
| 391 | #print >> sys.stderr, \ |
|---|
| 392 | # 'Unable to get imported names for %r line %s"' % ( |
|---|
| 393 | # node.modname, node.lineno) |
|---|
| 394 | for name in imported.wildcard_import_names(): |
|---|
| 395 | node.parent.set_local(name, node) |
|---|
| 396 | else: |
|---|
| 397 | |
|---|