| 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 | """astng manager: avoid multible astng build of a same module when |
|---|
| 14 | possible by providing a class responsible to get astng representation |
|---|
| 15 | from various source and using a cache of built modules) |
|---|
| 16 | |
|---|
| 17 | :author: Sylvain Thenault |
|---|
| 18 | :copyright: 2003-2007 LOGILAB S.A. (Paris, FRANCE) |
|---|
| 19 | :contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org |
|---|
| 20 | :copyright: 2003-2007 Sylvain Thenault |
|---|
| 21 | :contact: mailto:thenault@gmail.com |
|---|
| 22 | """ |
|---|
| 23 | |
|---|
| 24 | __docformat__ = "restructuredtext en" |
|---|
| 25 | |
|---|
| 26 | import sys |
|---|
| 27 | import os |
|---|
| 28 | from os.path import dirname, basename, abspath, join, isdir, exists |
|---|
| 29 | |
|---|
| 30 | from logilab.common.cache import Cache |
|---|
| 31 | from logilab.common.modutils import NoSourceFile, is_python_source, \ |
|---|
| 32 | file_from_modpath, load_module_from_name, \ |
|---|
| 33 | get_module_files, get_source_file |
|---|
| 34 | from logilab.common.configuration import OptionsProviderMixIn |
|---|
| 35 | |
|---|
| 36 | from logilab.astng import ASTNGBuildingException, Instance, nodes |
|---|
| 37 | |
|---|
| 38 | def astng_wrapper(func, modname): |
|---|
| 39 | """wrapper to give to ASTNGManager.project_from_files""" |
|---|
| 40 | print 'parsing %s...' % modname |
|---|
| 41 | try: |
|---|
| 42 | return func(modname) |
|---|
| 43 | except ASTNGBuildingException, ex: |
|---|
| 44 | print ex |
|---|
| 45 | except KeyboardInterrupt: |
|---|
| 46 | raise |
|---|
| 47 | except Exception, ex: |
|---|
| 48 | import traceback |
|---|
| 49 | traceback.print_exc() |
|---|
| 50 | |
|---|
| 51 | def safe_repr(obj): |
|---|
| 52 | try: |
|---|
| 53 | return repr(obj) |
|---|
| 54 | except: |
|---|
| 55 | return '???' |
|---|
| 56 | |
|---|
| 57 | class ASTNGManager(OptionsProviderMixIn): |
|---|
| 58 | """the astng manager, responsible to build astng from files |
|---|
| 59 | or modules. |
|---|
| 60 | |
|---|
| 61 | Use the Borg pattern. |
|---|
| 62 | """ |
|---|
| 63 | name = 'astng loader' |
|---|
| 64 | options = (("ignore", |
|---|
| 65 | {'type' : "csv", 'metavar' : "<file>", |
|---|
| 66 | 'dest' : "black_list", "default" : ('CVS',), |
|---|
| 67 | 'help' : "add <file> (may be a directory) to the black list\ |
|---|
| 68 | . It should be a base name, not a path. You may set this option multiple times\ |
|---|
| 69 | ."}), |
|---|
| 70 | ("project", |
|---|
| 71 | {'default': "No Name", 'type' : 'string', 'short': 'p', |
|---|
| 72 | 'metavar' : '<project name>', |
|---|
| 73 | 'help' : 'set the project name.'}), |
|---|
| 74 | ) |
|---|
| 75 | brain = {} |
|---|
| 76 | def __init__(self): |
|---|
| 77 | self.__dict__ = ASTNGManager.brain |
|---|
| 78 | if not self.__dict__: |
|---|
| 79 | OptionsProviderMixIn.__init__(self) |
|---|
| 80 | self._cache = None |
|---|
| 81 | self._mod_file_cache = None |
|---|
| 82 | self.set_cache_size(200) |
|---|
| 83 | self.load_defaults() |
|---|
| 84 | |
|---|
| 85 | def set_cache_size(self, cache_size): |
|---|
| 86 | """set the cache size (flush it as a side effect!)""" |
|---|
| 87 | self._cache = {} #Cache(cache_size) |
|---|
| 88 | self._mod_file_cache = {} |
|---|
| 89 | |
|---|
| 90 | def from_directory(self, directory, modname=None): |
|---|
| 91 | """given a module name, return the astng object""" |
|---|
| 92 | modname = modname or basename(directory) |
|---|
| 93 | directory = abspath(directory) |
|---|
| 94 | return Package(directory, modname, self) |
|---|
| 95 | |
|---|
| 96 | def astng_from_file(self, filepath, modname=None, fallback=True): |
|---|
| 97 | """given a module name, return the astng object""" |
|---|
| 98 | try: |
|---|
| 99 | filepath = get_source_file(filepath, include_no_ext=True) |
|---|
| 100 | source = True |
|---|
| 101 | except NoSourceFile: |
|---|
| 102 | source = False |
|---|
| 103 | try: |
|---|
| 104 | return self._cache[filepath] |
|---|
| 105 | except KeyError: |
|---|
| 106 | if source: |
|---|
| 107 | try: |
|---|
| 108 | from logilab.astng.builder import ASTNGBuilder |
|---|
| 109 | astng = ASTNGBuilder(self).file_build(filepath, modname) |
|---|
| 110 | except SyntaxError: |
|---|
| 111 | raise |
|---|
| 112 | except Exception, ex: |
|---|
| 113 | if __debug__: |
|---|
| 114 | import traceback |
|---|
| 115 | traceback.print_exc() |
|---|
| 116 | msg = 'Unable to load module %s (%s)' % (modname, ex) |
|---|
| 117 | raise ASTNGBuildingException(msg), None, sys.exc_info()[-1] |
|---|
| 118 | elif fallback and modname: |
|---|
| 119 | return self.astng_from_module_name(modname) |
|---|
| 120 | else: |
|---|
| 121 | raise ASTNGBuildingException('unable to get astng for file %s' % |
|---|
| 122 | filepath) |
|---|
| 123 | self._cache[filepath] = astng |
|---|
| 124 | return astng |
|---|
| 125 | |
|---|
| 126 | from_file = astng_from_file # backward compat |
|---|
| 127 | |
|---|
| 128 | def astng_from_module_name(self, modname, context_file=None): |
|---|
| 129 | """given a module name, return the astng object""" |
|---|
| 130 | old_cwd = os.getcwd() |
|---|
| 131 | if context_file: |
|---|
| 132 | os.chdir(dirname(context_file)) |
|---|
| 133 | try: |
|---|
| 134 | filepath = self.file_from_module_name(modname, context_file) |
|---|
| 135 | if filepath is None or not is_python_source(filepath): |
|---|
| 136 | try: |
|---|
| 137 | module = load_module_from_name(modname) |
|---|
| 138 | except ImportError, ex: |
|---|
| 139 | msg = 'Unable to load module %s (%s)' % (modname, ex) |
|---|
| 140 | raise ASTNGBuildingException(msg) |
|---|
| 141 | return self.astng_from_module(module, modname) |
|---|
| 142 | return self.astng_from_file(filepath, modname, fallback=False) |
|---|
| 143 | finally: |
|---|
| 144 | os.chdir(old_cwd) |
|---|
| 145 | |
|---|
| 146 | def file_from_module_name(self, modname, contextfile): |
|---|
| 147 | try: |
|---|
| 148 | value = self._mod_file_cache[(modname, contextfile)] |
|---|
| 149 | except KeyError: |
|---|
| 150 | try: |
|---|
| 151 | value = file_from_modpath(modname.split('.'), |
|---|
| 152 | context_file=contextfile) |
|---|
| 153 | except ImportError, ex: |
|---|
| 154 | msg = 'Unable to load module %s (%s)' % (modname, ex) |
|---|
| 155 | value = ASTNGBuildingException(msg) |
|---|
| 156 | self._mod_file_cache[(modname, contextfile)] = value |
|---|
| 157 | if isinstance(value, ASTNGBuildingException): |
|---|
| 158 | raise value |
|---|
| 159 | return value |
|---|
| 160 | |
|---|
| 161 | def astng_from_module(self, module, modname=None): |
|---|
| 162 | """given an imported module, return the astng object""" |
|---|
| 163 | modname = modname or module.__name__ |
|---|
| 164 | filepath = modname |
|---|
| 165 | try: |
|---|
| 166 | # some builtin modules don't have __file__ attribute |
|---|
| 167 | filepath = module.__file__ |
|---|
| 168 | if is_python_source(filepath): |
|---|
| 169 | return self.astng_from_file(filepath, modname) |
|---|
| 170 | except AttributeError: |
|---|
| 171 | pass |
|---|
| 172 | try: |
|---|
| 173 | return self._cache[filepath] |
|---|
| 174 | except KeyError: |
|---|
| 175 | from logilab.astng.builder import ASTNGBuilder |
|---|
| 176 | astng = ASTNGBuilder(self).module_build(module, modname) |
|---|
| 177 | # update caches (filepath and astng.file are not necessarily the |
|---|
| 178 | # same (.pyc pb)) |
|---|
| 179 | self._cache[filepath] = self._cache[astng.file] = astng |
|---|
| 180 | return astng |
|---|
| 181 | |
|---|
| 182 | def astng_from_class(self, klass, modname=None): |
|---|
| 183 | """get astng for the given class""" |
|---|
| 184 | if modname is None: |
|---|
| 185 | try: |
|---|
| 186 | modname = klass.__module__ |
|---|
| 187 | except AttributeError: |
|---|
| 188 | raise ASTNGBuildingException( |
|---|
| 189 | 'Unable to get module for class %s' % safe_repr(klass)) |
|---|
| 190 | modastng = self.astng_from_module_name(modname) |
|---|
| 191 | return modastng.getattr(klass.__name__)[0] # XXX |
|---|
| 192 | |
|---|
| 193 | |
|---|
| 194 | def infer_astng_from_something(self, obj, modname=None, context=None): |
|---|
| 195 | """infer astng for the given class""" |
|---|
| 196 | if hasattr(obj, '__class__') and not isinstance(obj, type): |
|---|
| 197 | klass = obj.__class__ |
|---|
| 198 | else: |
|---|
| 199 | klass = obj |
|---|
| 200 | if modname is None: |
|---|
| 201 | try: |
|---|
| 202 | modname = klass.__module__ |
|---|
| 203 | except AttributeError: |
|---|
| 204 | raise ASTNGBuildingException( |
|---|
| 205 | 'Unable to get module for %s' % safe_repr(klass)) |
|---|
| 206 | except Exception, ex: |
|---|
| 207 | raise ASTNGBuildingException( |
|---|
| 208 | 'Unexpected error while retreiving module for %s: %s' |
|---|
| 209 | % (safe_repr(klass), ex)) |
|---|
| 210 | try: |
|---|
| 211 | name = klass.__name__ |
|---|
| 212 | except AttributeError: |
|---|
| 213 | raise ASTNGBuildingException( |
|---|
| 214 | 'Unable to get name for %s' % safe_repr(klass)) |
|---|
| 215 | except Exception, ex: |
|---|
| 216 | raise ASTNGBuildingException( |
|---|
| 217 | 'Unexpected error while retreiving name for %s: %s' |
|---|
| 218 | % (safe_repr(klass), ex)) |
|---|
| 219 | # take care, on living object __module__ is regularly wrong :( |
|---|
| 220 | modastng = self.astng_from_module_name(modname) |
|---|
| 221 | for infered in modastng.igetattr(name, context): |
|---|
| 222 | if klass is not obj and isinstance(infered, nodes.Class): |
|---|
| 223 | infered = Instance(infered) |
|---|
| 224 | yield infered |
|---|
| 225 | |
|---|
| 226 | def project_from_files(self, files, func_wrapper=astng_wrapper, |
|---|
| 227 | project_name=None, black_list=None): |
|---|
| 228 | """return a Project from a list of files or modules""" |
|---|
| 229 | # insert current working directory to the python path to have a correct |
|---|
| 230 | # behaviour |
|---|
| 231 | sys.path.insert(0, os.getcwd()) |
|---|
| 232 | try: |
|---|
| 233 | # build the project representation |
|---|
| 234 | project_name = project_name or self.config.project |
|---|
| 235 | black_list = black_list or self.config.black_list |
|---|
| 236 | project = Project(project_name) |
|---|
| 237 | for something in files: |
|---|
| 238 | if not exists(something): |
|---|
| 239 | fpath = file_from_modpath(something.split('.')) |
|---|
| 240 | elif isdir(something): |
|---|
| 241 | fpath = join(something, '__init__.py') |
|---|
| 242 | else: |
|---|
| 243 | fpath = something |
|---|
| 244 | astng = func_wrapper(self.astng_from_file, fpath) |
|---|
| 245 | if astng is None: |
|---|
| 246 | continue |
|---|
| 247 | project.path = project.path or astng.file |
|---|
| 248 | project.add_module(astng) |
|---|
| 249 | base_name = astng.name |
|---|
| 250 | # recurse in package except if __init__ was explicitly given |
|---|
| 251 | if astng.package and something.find('__init__') == -1: |
|---|
| 252 | # recurse on others packages / modules if this is a package |
|---|
| 253 | for fpath in get_module_files(dirname(astng.file), |
|---|
| 254 | black_list): |
|---|
| 255 | astng = func_wrapper(self.astng_from_file, fpath) |
|---|
| 256 | if astng is None or astng.name == base_name: |
|---|
| 257 | continue |
|---|
| 258 | project.add_module(astng) |
|---|
| 259 | return project |
|---|
| 260 | finally: |
|---|
| 261 | sys.path.pop(0) |
|---|
| 262 | |
|---|
| 263 | |
|---|
| 264 | |
|---|
| 265 | class Package: |
|---|
| 266 | """a package using a dictionary like interface |
|---|
| 267 | |
|---|
| 268 | load submodules lazily, as they are needed |
|---|
| 269 | """ |
|---|
| 270 | |
|---|
| 271 | def __init__(self, path, name, manager): |
|---|
| 272 | self.name = name |
|---|
| 273 | self.path = abspath(path) |
|---|
| 274 | self.manager = manager |
|---|
| 275 | self.parent = None |
|---|
| 276 | self.lineno = 0 |
|---|
| 277 | self.__keys = None |
|---|
| 278 | self.__subobjects = None |
|---|
| 279 | |
|---|
| 280 | def fullname(self): |
|---|
| 281 | """return the full name of the package (i.e. prefix by the full name |
|---|
| 282 | of the parent package if any |
|---|
| 283 | """ |
|---|
| 284 | if self.parent is None: |
|---|
| 285 | return self.name |
|---|
| 286 | return '%s.%s' % (self.parent.fullname(), self.name) |
|---|
| 287 | |
|---|
| 288 | def get_subobject(self, name): |
|---|
| 289 | """method used to get sub-objects lazily : sub package or module are |
|---|
| 290 | only build once they are requested |
|---|
| 291 | """ |
|---|
| 292 | if self.__subobjects is None: |
|---|
| 293 | try: |
|---|
| 294 | self.__subobjects = dict.fromkeys(self.keys()) |
|---|
| 295 | except AttributeError: |
|---|
| 296 | # python <= 2.3 |
|---|
| 297 | self.__subobjects = dict([(k, None) for k in self.keys()]) |
|---|
| 298 | obj = self.__subobjects[name] |
|---|
| 299 | if obj is None: |
|---|
| 300 | objpath = join(self.path, name) |
|---|
| 301 | if isdir(objpath): |
|---|
| 302 | obj = Package(objpath, name, self.manager) |
|---|
| 303 | obj.parent = self |
|---|
| 304 | else: |
|---|
| 305 | modname = '%s.%s' % (self.fullname(), name) |
|---|
| 306 | obj = self.manager.astng_from_file(objpath + '.py', modname) |
|---|
| 307 | self.__subobjects[name] = obj |
|---|
| 308 | return obj |
|---|
| 309 | |
|---|
| 310 | def get_module(self, modname): |
|---|
| 311 | """return the Module or Package object with the given name if any |
|---|
| 312 | """ |
|---|
| 313 | path = modname.split('.') |
|---|
| 314 | if path[0] != self.name: |
|---|
| 315 | raise KeyError(modname) |
|---|
| 316 | obj = self |
|---|
| 317 | for part in path[1:]: |
|---|
| 318 | obj = obj.get_subobject(part) |
|---|
| 319 | return obj |
|---|
| 320 | |
|---|
| 321 | def keys(self): |
|---|
| 322 | if self.__keys is None: |
|---|
| 323 | self.__keys = [] |
|---|
| 324 | for fname in os.listdir(self.path): |
|---|
| 325 | if fname.endswith('.py'): |
|---|
| 326 | self.__keys.append(fname[:-3]) |
|---|
| 327 | continue |
|---|
| 328 | fpath = join(self.path, fname) |
|---|
| 329 | if isdir(fpath) and exists(join(fpath, '__init__.py')): |
|---|
| 330 | self.__keys.append(fname) |
|---|
| 331 | self.__keys.sort() |
|---|
| 332 | return self.__keys[:] |
|---|
| 333 | |
|---|
| 334 | def values(self): |
|---|
| 335 | return [self.get_subobject(name) for name in self.keys()] |
|---|
| 336 | |
|---|
| 337 | def items(self): |
|---|
| 338 | return zip(self.keys(), self.values()) |
|---|
| 339 | |
|---|
| 340 | def has_key(self, name): |
|---|
| 341 | return bool(self.get(name)) |
|---|
| 342 | |
|---|
| 343 | def get(self, name, default=None): |
|---|
| 344 | try: |
|---|
| 345 | return self.get_subobject(name) |
|---|
| 346 | except KeyError: |
|---|
| 347 | return default |
|---|
| 348 | |
|---|
| 349 | def __getitem__(self, name): |
|---|
| 350 | return self.get_subobject(name) |
|---|
| 351 | def __contains__(self, name): |
|---|
| 352 | return self.has_key(name) |
|---|
| 353 | def __iter__(self): |
|---|
| 354 | return iter(self.keys()) |
|---|
| 355 | |
|---|
| 356 | |
|---|
| 357 | class Project: |
|---|
| 358 | """a project handle a set of modules / packages""" |
|---|
| 359 | def __init__(self, name=''): |
|---|
| 360 | self.name = name |
|---|
| 361 | self.path = None |
|---|
| 362 | self.modules = [] |
|---|
| 363 | self.locals = {} |
|---|
| 364 | self.__getitem__ = self.locals.__getitem__ |
|---|
| 365 | self.__iter__ = self.locals.__iter__ |
|---|
| 366 | self.values = self.locals.values |
|---|
| 367 | self.keys = self.locals.keys |
|---|
| 368 | self.has_key = self.locals.has_key |
|---|
| 369 | |
|---|
| 370 | def add_module(self, node): |
|---|
| 371 | self.locals[node.name] = node |
|---|
| 372 | self.modules.append(node) |
|---|
| 373 | |
|---|
| 374 | def get_module(self, name): |
|---|
| 375 | return self.locals[name] |
|---|
| 376 | |
|---|
| 377 | def getChildNodes(self): |
|---|
| 378 | return self.modules |
|---|
| 379 | |
|---|
| 380 | def __repr__(self): |
|---|
| 381 | return '<Project %r at %s (%s modules)>' % (self.name, id(self), |
|---|
| 382 | len(self.modules)) |
|---|