# This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; either version 2 of the License, or (at your option) any later # version. # # This program 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 General Public License for more details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. """astng manager: avoid multible astng build of a same module when possible by providing a class responsible to get astng representation from various source and using a cache of built modules) :author: Sylvain Thenault :copyright: 2003-2009 LOGILAB S.A. (Paris, FRANCE) :contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org :copyright: 2003-2009 Sylvain Thenault :contact: mailto:thenault@gmail.com """ __docformat__ = "restructuredtext en" import sys import os from os.path import dirname, basename, abspath, join, isdir, exists from logilab.common.modutils import NoSourceFile, is_python_source, \ file_from_modpath, load_module_from_name, \ get_module_files, get_source_file, zipimport from logilab.common.configuration import OptionsProviderMixIn from logilab.astng import ASTNGBuildingException, nodes, infutils def astng_wrapper(func, modname): """wrapper to give to ASTNGManager.project_from_files""" print 'parsing %s...' % modname try: return func(modname) except ASTNGBuildingException, ex: print ex except KeyboardInterrupt: raise except Exception, ex: import traceback traceback.print_exc() def safe_repr(obj): try: return repr(obj) except: return '???' def zip_import_data(filepath): if zipimport is None: return None, None for ext in ('.zip', '.egg'): try: eggpath, resource = filepath.split(ext + '/', 1) except ValueError: continue try: importer = zipimport.zipimporter(eggpath + ext) return importer.get_source(resource), resource.replace('/', '.') except: continue return None, None class ASTNGManager(OptionsProviderMixIn): """the astng manager, responsible to build astng from files or modules. Use the Borg pattern. """ name = 'astng loader' options = (("ignore", {'type' : "csv", 'metavar' : "", 'dest' : "black_list", "default" : ('CVS',), 'help' : "add (may be a directory) to the black list\ . It should be a base name, not a path. You may set this option multiple times\ ."}), ("project", {'default': "No Name", 'type' : 'string', 'short': 'p', 'metavar' : '', 'help' : 'set the project name.'}), ) brain = {} def __init__(self, borg=True): if borg: self.__dict__ = ASTNGManager.brain if not self.__dict__: OptionsProviderMixIn.__init__(self) self._cache = None self._mod_file_cache = None self.set_cache_size(200) self.load_defaults() def set_cache_size(self, cache_size): """set the cache size (flush it as a side effect!)""" self._cache = {} #Cache(cache_size) self._mod_file_cache = {} def from_directory(self, directory, modname=None): """given a module name, return the astng object""" modname = modname or basename(directory) directory = abspath(directory) return Package(directory, modname, self) def astng_from_file(self, filepath, modname=None, fallback=True): """given a module name, return the astng object""" try: filepath = get_source_file(filepath, include_no_ext=True) source = True except NoSourceFile: source = False try: return self._cache[filepath] except KeyError: if source: try: from logilab.astng.builder import ASTNGBuilder astng = ASTNGBuilder(self).file_build(filepath, modname) except (SyntaxError, KeyboardInterrupt, SystemExit): raise except Exception, ex: if __debug__: print 'error while building astng for', filepath import traceback traceback.print_exc() msg = 'Unable to load module %s (%s)' % (modname, ex) raise ASTNGBuildingException(msg), None, sys.exc_info()[-1] elif fallback and modname: return self.astng_from_module_name(modname) else: raise ASTNGBuildingException('unable to get astng for file %s' % filepath) self._cache[filepath] = astng return astng from_file = astng_from_file # backward compat def astng_from_module_name(self, modname, context_file=None): """given a module name, return the astng object""" old_cwd = os.getcwd() if context_file: os.chdir(dirname(context_file)) try: filepath = self.file_from_module_name(modname, context_file) if filepath is not None and not is_python_source(filepath): try: return self._cache[filepath] except KeyError: data, zmodname = zip_import_data(filepath) if data is not None: from logilab.astng.builder import ASTNGBuilder try: astng = ASTNGBuilder(self).string_build(data, zmodname, filepath) except (SyntaxError, KeyboardInterrupt, SystemExit): raise self._cache[filepath] = astng return astng if filepath is None or not is_python_source(filepath): try: module = load_module_from_name(modname) except ImportError, ex: msg = 'Unable to load module %s (%s)' % (modname, ex) raise ASTNGBuildingException(msg) return self.astng_from_module(module, modname) return self.astng_from_file(filepath, modname, fallback=False) finally: os.chdir(old_cwd) def file_from_module_name(self, modname, contextfile): try: value = self._mod_file_cache[(modname, contextfile)] except KeyError: try: value = file_from_modpath(modname.split('.'), context_file=contextfile) except ImportError, ex: msg = 'Unable to load module %s (%s)' % (modname, ex) value = ASTNGBuildingException(msg) self._mod_file_cache[(modname, contextfile)] = value if isinstance(value, ASTNGBuildingException): raise value return value def astng_from_module(self, module, modname=None): """given an imported module, return the astng object""" modname = modname or module.__name__ filepath = modname try: # some builtin modules don't have __file__ attribute filepath = module.__file__ if is_python_source(filepath): return self.astng_from_file(filepath, modname) except AttributeError: pass try: return self._cache[filepath] except KeyError: from logilab.astng.builder import ASTNGBuilder astng = ASTNGBuilder(self).module_build(module, modname) # update caches (filepath and astng.file are not necessarily the # same (.pyc pb)) self._cache[filepath] = self._cache[astng.file] = astng return astng def astng_from_class(self, klass, modname=None): """get astng for the given class""" if modname is None: try: modname = klass.__module__ except AttributeError: raise ASTNGBuildingException( 'Unable to get module for class %s' % safe_repr(klass)) modastng = self.astng_from_module_name(modname) return modastng.getattr(klass.__name__)[0] # XXX def infer_astng_from_something(self, obj, modname=None, context=None): """infer astng for the given class""" if hasattr(obj, '__class__') and not isinstance(obj, type): klass = obj.__class__ else: klass = obj if modname is None: try: modname = klass.__module__ except AttributeError: raise ASTNGBuildingException( 'Unable to get module for %s' % safe_repr(klass)) except Exception, ex: raise ASTNGBuildingException( 'Unexpected error while retreiving module for %s: %s' % (safe_repr(klass), ex)) try: name = klass.__name__ except AttributeError: raise ASTNGBuildingException( 'Unable to get name for %s' % safe_repr(klass)) except Exception, ex: raise ASTNGBuildingException( 'Unexpected error while retreiving name for %s: %s' % (safe_repr(klass), ex)) # take care, on living object __module__ is regularly wrong :( modastng = self.astng_from_module_name(modname) for infered in modastng.igetattr(name, context): if klass is not obj and isinstance(infered, nodes.Class): infered = infutils.Instance(infered) yield infered def project_from_files(self, files, func_wrapper=astng_wrapper, project_name=None, black_list=None): """return a Project from a list of files or modules""" # insert current working directory to the python path to have a correct # behaviour sys.path.insert(0, os.getcwd()) try: # build the project representation project_name = project_name or self.config.project black_list = black_list or self.config.black_list project = Project(project_name) for something in files: if not exists(something): fpath = file_from_modpath(something.split('.')) elif isdir(something): fpath = join(something, '__init__.py') else: fpath = something astng = func_wrapper(self.astng_from_file, fpath) if astng is None: continue project.path = project.path or astng.file project.add_module(astng) base_name = astng.name # recurse in package except if __init__ was explicitly given if astng.package and something.find('__init__') == -1: # recurse on others packages / modules if this is a package for fpath in get_module_files(dirname(astng.file), black_list): astng = func_wrapper(self.astng_from_file, fpath) if astng is None or astng.name == base_name: continue project.add_module(astng) return project finally: sys.path.pop(0) class Package: """a package using a dictionary like interface load submodules lazily, as they are needed """ def __init__(self, path, name, manager): self.name = name self.path = abspath(path) self.manager = manager self.parent = None self.lineno = 0 self.__keys = None self.__subobjects = None def fullname(self): """return the full name of the package (i.e. prefix by the full name of the parent package if any """ if self.parent is None: return self.name return '%s.%s' % (self.parent.fullname(), self.name) def get_subobject(self, name): """method used to get sub-objects lazily : sub package or module are only build once they are requested """ if self.__subobjects is None: try: self.__subobjects = dict.fromkeys(self.keys()) except AttributeError: # python <= 2.3 self.__subobjects = dict([(k, None) for k in self.keys()]) obj = self.__subobjects[name] if obj is None: objpath = join(self.path, name) if isdir(objpath): obj = Package(objpath, name, self.manager) obj.parent = self else: modname = '%s.%s' % (self.fullname(), name) obj = self.manager.astng_from_file(objpath + '.py', modname) self.__subobjects[name] = obj return obj def get_module(self, modname): """return the Module or Package object with the given name if any """ path = modname.split('.') if path[0] != self.name: raise KeyError(modname) obj = self for part in path[1:]: obj = obj.get_subobject(part) return obj def keys(self): if self.__keys is None: self.__keys = [] for fname in os.listdir(self.path): if fname.endswith('.py'): self.__keys.append(fname[:-3]) continue fpath = join(self.path, fname) if isdir(fpath) and exists(join(fpath, '__init__.py')): self.__keys.append(fname) self.__keys.sort() return self.__keys[:] def values(self): return [self.get_subobject(name) for name in self.keys()] def items(self): return zip(self.keys(), self.values()) def has_key(self, name): return bool(self.get(name)) def get(self, name, default=None): try: return self.get_subobject(name) except KeyError: return default def __getitem__(self, name): return self.get_subobject(name) def __contains__(self, name): return self.has_key(name) def __iter__(self): return iter(self.keys()) class Project: """a project handle a set of modules / packages""" def __init__(self, name=''): self.name = name self.path = None self.modules = [] self.locals = {} self.__getitem__ = self.locals.__getitem__ self.__iter__ = self.locals.__iter__ self.values = self.locals.values self.keys = self.locals.keys self.items = self.locals.items self.has_key = self.locals.has_key def add_module(self, node): self.locals[node.name] = node self.modules.append(node) def get_module(self, name): return self.locals[name] def get_children(self): return self.modules def __repr__(self): return '' % (self.name, id(self), len(self.modules))