diff options
author | root <none@none> | 2006-04-26 10:48:09 +0000 |
---|---|---|
committer | root <none@none> | 2006-04-26 10:48:09 +0000 |
commit | 85529cfcb8e870333d7292cb493f54d7f3fd92cf (patch) | |
tree | ab1c5d7f169245ebf38ec6c8dba5065cbd48ec9f | |
download | astroid-git-85529cfcb8e870333d7292cb493f54d7f3fd92cf.tar.gz |
forget the past.
forget the past.
56 files changed, 6451 insertions, 0 deletions
diff --git a/.hgignore b/.hgignore new file mode 100644 index 00000000..2849616a --- /dev/null +++ b/.hgignore @@ -0,0 +1,4 @@ +(^|/)\.svn($|/) +(^|/)\.hg($|/) +(^|/)\.hgtags($|/) +^log$ diff --git a/ChangeLog b/ChangeLog new file mode 100644 index 00000000..d5e3ccb7 --- /dev/null +++ b/ChangeLog @@ -0,0 +1,124 @@ +Change log for the astng package +================================ + +2006-04-19 -- 0.16.0 + * fix living object building to consider classes such as property as + a class instead of a data descriptor + + * fix multiple assignment inference which was discarding some solutions + + * added some line manipulation methods to handle pylint's block messages + control feature (Node.last_source_line(), None.block_range(lineno) + + +2006-03-10 -- 0.15.1 + * fix avoiding to load everything from living objects... Thanks Amaury! + + * fix a possible NameError in Instance.infer_call_result + + +2006-03-06 -- 0.15.0 + * fix possible infinite recursion on global statements (close #10342) + and in various other cases... + + * fix locals/globals interactions when the global statement is used + (close #10434) + + * multiple inference related bug fixes + + * associate List, Tuple and Dict and Const nodes to their respective + classes + + * new .ass_type method on assignment related node, returning the + assigment type node (Assign, For, ListCompFor, GenExprFor, + TryExcept) + + * more API refactoring... .resolve method has disappeared, now you + have .ilookup on every nodes and .getattr/.igetattr on node + supporting the attribute protocol + + * introduced a YES object that may be returned when there is ambiguity + on an inference path (typically function call when we don't know + arguments value) + + * builder try to instantiate builtin exceptions subclasses to get their + instance attribute + + +2006-01-10 -- 0.14.0 + * some major inference improvments and refactoring ! The drawback is + the introduction of some non backward compatible change in the API + but it's imho much cleaner and powerful now :) + + * new boolean property .newstyle on Class nodes (implements #10073) + + * new .import_module method on Module node to help in .resolve + refactoring + + * .instance_attrs has list of assignments to instance attribute + dictionary as value instead of one + + * added missing GenExprIf and GenExprInner nodes, and implements + as_string for each generator expression related nodes + + * specifically catch KeyboardInterrupt to reraise it in some places + + * fix so that module names are always absolute + + * fix .resolve on package where a subpackage is imported in the + __init__ file + + * fix a bug regarding construction of Function node from living object + with realier version of python 2.4 + + * fix a NameError on Import and From self_resolve method + + * fix a bug occuring when building an astng from a living object with + a property + + * lint fixes + + +2005-11-07 -- 0.13.1 + * fix bug on building from living module the same object in + encountered more than once time (eg builtins.object) (close #10069) + + * fix bug in Class.ancestors() regarding inner classes (close #10072) + + * fix .self_resolve() on From and Module nodes to handle package + precedence over module (close #10066) + + * locals dict for package contains __path__ definition (close #10065) + + * astng provide GenExpr and GenExprFor nodes with python >= 2.4 + (close #10063) + + * fix python2.2 compatibility (close #9922) + + * link .__contains__ to .has_key on scoped node to speed up execution + + * remove no more necessary .module_object() method on From and Module + nodes + + * normalize parser.ParserError to SyntaxError with python 2.2 + + +2005-10-21 -- 0.13.0 + * .locals and .globals on scoped node handle now a list of references + to each assigment statements instead of a single reference to the + first assigment statement. + + * fix bug with manager.astng_from_module_name when a context file is + given (notably fix ZODB 3.4 crash with pylint/pyreverse) + + * fix Compare.as_string method + + * fix bug with lambda object missing the "type" attribute + + * some minor refactoring + + * This package has been extracted from the logilab-common package, which + will be kept for some time for backward compatibility but will no + longer be maintained (this explains that this package is starting with + the 0.13 version number, since the fork occurs with the version + released in logilab-common 0.12). diff --git a/DEPENDS b/DEPENDS new file mode 100644 index 00000000..a2522761 --- /dev/null +++ b/DEPENDS @@ -0,0 +1 @@ +python-logilab-common diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 00000000..380fbf8f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +include ChangeLog +include README +include COPYING +include DEPENDS +include test/fulltest.sh +recursive-include test/data *.py +recursive-include test/data2 *.py +recursive-include test/regrtest_data *.py @@ -0,0 +1,52 @@ +ASTNG +===== + +What's this ? +------------- + +The aim of this module is to provide a common base representation of +python source code for projects such as pychecker, pyreverse, +pylint... Well, actually the development of this library is essentialy +governed by pylint's needs. + +It extends class defined in the compiler.ast [1] module with some +additional methods and attributes. Instance attributes are added by a +builder object, which can either generate extended ast (let's call +them astng ;) by visiting an existant ast tree or by inspecting living +object. Methods are added by monkey patching ast classes. + +Main modules are: + +* nodes and scoped_nodes for more information about methods and + attributes added to different node classes + +* the manager contains a high level object to get astng trees from + source files and living objects. It maintains a cache of previously + constructed tree for quick access + +* builder contains the class responsible to build astng trees + + +Notice +------ +This package has been extracted from the logilab-common package, which +will be kept for some time for backward compatibility but will no +longer be maintained. + + +Installation +------------ + +Extract the tarball, jump into the created directory and run :: + + python setup.py install + +For installation options, see :: + + python setup.py install --help + + +If you have any questions, please mail devel@logilab.fr for support. + +Sylvain Thénault +Oct 21, 2005 diff --git a/__init__.py b/__init__.py new file mode 100644 index 00000000..a06671eb --- /dev/null +++ b/__init__.py @@ -0,0 +1,207 @@ +# 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. +"""Python Abstract Syntax Tree New Generation + +The aim of this module is to provide a common base representation of +python source code for projects such as pychecker, pyreverse, +pylint... Well, actually the development of this library is essentialy +governed by pylint's needs. + +It extends class defined in the compiler.ast [1] module with some +additional methods and attributes. Instance attributes are added by a +builder object, which can either generate extended ast (let's call +them astng ;) by visiting an existant ast tree or by inspecting living +object. Methods are added by monkey patching ast classes. + +Main modules are: + +* nodes and scoped_nodes for more information about methods and + attributes added to different node classes + +* the manager contains a high level object to get astng trees from + source files and living objects. It maintains a cache of previously + constructed tree for quick access + +* builder contains the class responsible to build astng trees + + +:version: $Revision: 1.24 $ +:author: Sylvain Thenault +:copyright: 2003-2006 LOGILAB S.A. (Paris, FRANCE) +:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org +:copyright: 2003-2006 Sylvain Thenault +:contact: mailto:thenault@gmail.com +""" + +from __future__ import generators + +__revision__ = "$Id: __init__.py,v 1.24 2006-04-20 07:37:28 syt Exp $" +__doctype__ = "restructuredtext en" + +from logilab.common.compat import chain, imap + +# WARNING: internal imports order matters ! + +from logilab.astng._exceptions import * + + +def unpack_infer(stmt, path=None): + """return an iterator on nodes infered by the given statement + if the infered value is a list or a tuple, recurse on it to + get values infered by its content + """ + if isinstance(stmt, (List, Tuple)): + return chain(*imap(unpack_infer, stmt.nodes)) + infered = stmt.infer(path=path).next() + if infered is stmt: + return iter( (stmt,) ) + return chain(*imap(unpack_infer, stmt.infer(path=path))) + +def _infer_stmts(stmts, name=None, path=None, frame=None): + """return an iterator on statements infered by each statement in <stmts> + """ + stmt = None + one_infered = False + for stmt in stmts: + if stmt is YES: + yield stmt + one_infered = True + continue + try: + for infered in stmt.infer(stmt._infer_name(frame, name), path): + yield infered + one_infered = True + except UnresolvableName: + continue + except InferenceError: + yield YES + one_infered = True + if not one_infered: + raise InferenceError(str(stmt)) + +# special inference objects ################################################### + +class Yes(object): + """a yes object""" + def __str__(self): + return 'YES' + def __getattribute__(self, name): + return self + def __call__(self, *args, **kwargs): + return self +YES = Yes() + +class Proxy: + """a simple proxy object""" + def __init__(self, proxied): + self._proxied = proxied + + def __getattr__(self, name): + return getattr(self._proxied, name) + + def infer(self, name=None, path=None): + yield self + +class Instance(Proxy): + """a special node representing a class instance""" + def getattr(self, name, path=None, lookupclass=True): + try: + return self._proxied.instance_attr(name, path) + except NotFoundError: + if name == '__class__': + return [self._proxied] + if name == '__name__': + # access to __name__ gives undefined member on class + # instances but not on class objects + raise NotFoundError(name) + if lookupclass: + return self._proxied.getattr(name, path) + raise NotFoundError(name) + + def igetattr(self, name, path=None): + """infered getattr""" + try: + # XXX frame should be self._proxied, or not ? + return _infer_stmts(self.getattr(name, path, lookupclass=False), name, + frame=self, path=path) + except NotFoundError: + try: + # fallback to class'igetattr since it has some logic to handle + # descriptors + return self._proxied.igetattr(name, path=path) + except NotFoundError: + raise InferenceError(name) + + def infer_call_result(self, caller, inf_path=None): + """infer what's a class instance is returning when called""" + one_infered = False + for node in self._proxied.igetattr('__call__', inf_path): + for res in node.infer_call_result(caller, inf_path): + one_infered = True + yield res + if not one_infered: + raise InferenceError() + + def __repr__(self): + return 'Instance of %s' % self._proxied.name + + def callable(self): + try: + self._proxied.getattr('__call__') + return True + except NotFoundError: + return False + +class Generator(Proxy): + """a special node representing a generator""" + def callable(self): + return True + +# imports ##################################################################### + +from logilab.astng.manager import ASTNGManager, Project, Package +MANAGER = ASTNGManager() + +from logilab.astng.nodes import * +from logilab.astng import nodes +from logilab.astng.scoped_nodes import * +from logilab.astng import inference +from logilab.astng import lookup +lookup._decorate(nodes) + +List._proxied = MANAGER.astng_from_class(list) +List.__bases__ += (inference.Instance,) +Tuple._proxied = MANAGER.astng_from_class(tuple) +Tuple.__bases__ += (inference.Instance,) +Dict._proxied = MANAGER.astng_from_class(dict) +Dict.__bases__ += (inference.Instance,) +Dict._proxied = MANAGER.astng_from_class(dict) + +builtin_astng = Dict._proxied.root() + +Const.__bases__ += (inference.Instance,) +Const._proxied = None +def Const___getattr__(self, name): + if self.value is None: + raise AttributeError(name) + if self._proxied is None: + self._proxied = MANAGER.astng_from_class(self.value.__class__) + return getattr(self._proxied, name) +Const.__getattr__ = Const___getattr__ +def Const_getattr(self, name, path=None, lookupclass=None): + if self.value is None: + raise NotFoundError(name) + if self._proxied is None: + self._proxied = MANAGER.astng_from_class(self.value.__class__) + return self._proxied.getattr(name, path) +Const.getattr = Const_getattr diff --git a/__pkginfo__.py b/__pkginfo__.py new file mode 100644 index 00000000..a79af825 --- /dev/null +++ b/__pkginfo__.py @@ -0,0 +1,61 @@ +# pylint: disable-msg=W0622 +# +# Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# 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. +""" +logilab.astng packaging information +""" + +__revision__ = "$Id: __pkginfo__.py,v 1.12 2006-04-19 14:31:37 syt Exp $" + +modname = 'astng' +numversion = (0, 16, 0) +version = '.'.join([str(num) for num in numversion]) +pyversions = ["2.2", "2.3", "2.4"] + +license = 'GPL' +copyright = '''Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). +http://www.logilab.fr/ -- mailto:contact@logilab.fr''' + +author = 'Sylvain Thenault' +author_email = 'sylvain.thenault@logilab.fr' + +short_desc = "extend python's abstract syntax tree" + +long_desc = """The aim of this module is to provide a common base \ +representation of +python source code for projects such as pychecker, pyreverse, +pylint... Well, actually the development of this library is essentialy +governed by pylint's needs. + +It extends class defined in the compiler.ast [1] module with some +additional methods and attributes. Instance attributes are added by a +builder object, which can either generate extended ast (let's call +them astng ;) by visiting an existant ast tree or by inspecting living +object. Methods are added by monkey patching ast classes.""" + + +web = "http://www.logilab.org/projects/%s" % modname +ftp = "ftp://ftp.logilab.org/pub/%s" % modname +mailinglist = "mailto://python-projects@lists.logilab.org" + +subpackage_of = 'logilab' + +from os.path import join +include_dirs = [join('test', 'regrtest_data'), + join('test', 'data'), join('test', 'data2')] + +debian_uploader = 'Alexandre Fayolle <afayolle@debian.org>' diff --git a/_exceptions.py b/_exceptions.py new file mode 100644 index 00000000..8e2ee582 --- /dev/null +++ b/_exceptions.py @@ -0,0 +1,54 @@ +# 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. +"""this module contains exceptions used in the astng library + +:version: $Revision: 1.4 $ +:author: Sylvain Thenault +:copyright: 2003-2006 LOGILAB S.A. (Paris, FRANCE) +:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org +:copyright: 2003-2006 Sylvain Thenault +:contact: mailto:thenault@gmail.com +""" + +__revision__ = "$Id: _exceptions.py,v 1.4 2006-03-06 08:57:52 syt Exp $" +__doctype__ = "restructuredtext en" + +class ASTNGError(Exception): + """base exception class for all astng related exceptions + """ + +class ASTNGBuildingException(ASTNGError): + """exception class when we are not able to build an astng representation""" + +class ResolveError(ASTNGError): + """base class of astng resolution/inference error""" + +class NotFoundError(ResolveError): + """raised when we are unabled to resolve a name""" + +class InferenceError(ResolveError): + """raised when we are unabled to infer a node""" + +class UnresolvableName(InferenceError): + """raised when we are unabled to resolve a name""" + + +class NoDefault(ASTNGError): + """raised by function's `default_value` method when an argument has + no default value + """ + +class IgnoreChild(Exception): + """exception that maybe raised by visit methods to avoid children traversal + """ + diff --git a/announce.txt b/announce.txt new file mode 100644 index 00000000..27ebadb1 --- /dev/null +++ b/announce.txt @@ -0,0 +1,23 @@ +What's new ? +------------ +%CHANGELOG% + + +What is %SOURCEPACKAGE% ? +------------------------ +%LONG_DESC% + + +Home page +--------- +%WEB% + +Download +-------- +%FTP% + +Mailing list +------------ +%MAILINGLIST% + +%ADDITIONAL_DESCR%
\ No newline at end of file diff --git a/builder.py b/builder.py new file mode 100644 index 00000000..e296e64c --- /dev/null +++ b/builder.py @@ -0,0 +1,488 @@ +# 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. +"""The ASTNGBuilder makes astng from living object and / or from compiler.ast + +The builder is not thread safe and can't be used to parse different sources +at the same time. + +TODO: + - more complet representation on inspect build + (imported modules ? use dis.dis ?) + + +:version: $Revision: 1.54 $ +:author: Sylvain Thenault +:copyright: 2003-2005 LOGILAB S.A. (Paris, FRANCE) +:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org +:copyright: 2003-2005 Sylvain Thenault +:contact: mailto:thenault@gmail.com +""" + +__revision__ = "$Id: builder.py,v 1.54 2006-03-14 15:21:33 syt Exp $" +__doctype__ = "restructuredtext en" + +import sys +from os.path import splitext, basename, dirname, exists, abspath +from parser import ParserError +from compiler import parse +from inspect import isfunction, ismethod, ismethoddescriptor, isclass, \ + isbuiltin +try: # python 2.2 inspect module doesn't have the isdatadescriptor function + from inspect import isdatadescriptor +except ImportError: + def isdatadescriptor(_): + """fake isdatadescriptor function, always returning False""" + return False + +from logilab.common.fileutils import norm_read +from logilab.common.modutils import modpath_from_file + +from logilab.astng import nodes +from logilab.astng.utils import ASTWalker +from logilab.astng._exceptions import ASTNGBuildingException +from logilab.astng.raw_building import * + +# ast NG builder ############################################################## + +class ASTNGBuilder: + """provide astng building methods + """ + + def __init__(self): # XXX _ was the manager, keep for bw compat + self._module = None + self._file = None + self._done = None + self._stack, self._par_stack = None, None + self._metaclass = None + self._walker = ASTWalker(self) + self._dyn_modname_map = {'gtk': 'gtk._gtk'} + + def module_build(self, module, modname=None): + """build an astng from a living module instance + """ + node = None + self._module = module + path = getattr(module, '__file__', None) + if path is not None: + path_, ext = splitext(module.__file__) + if ext in ('.py', '.pyc', '.pyo') and 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 = self.inspect_build(module, modname=modname, path=path) + return node + + def inspect_build(self, module, modname=None, path=None): + """build astng from a living module (i.e. using inspect) + this is used when there is no python source code available (either + because it's a built-in module or because the .py is not available) + """ + self._module = module + node = build_module(modname or module.__name__, module.__doc__) + node.file = node.path = path and abspath(path) or path + node.package = hasattr(module, '__path__') + attach___dict__(node) + self._done = {} + self.object_build(node, module) + return node + + def file_build(self, path, modname=None): + """build astng from a source code file (i.e. from an ast) + + path is expected to be a python source file + """ + try: + data = norm_read(path) + except IOError, ex: + msg = 'Unable to load file %r (%s)' % (path, ex) + raise ASTNGBuildingException(msg) + self._file = path + # get module name if necessary, *before modifying sys.path* + if modname is None: + try: + modname = '.'.join(modpath_from_file(path)) + except ImportError: + modname = splitext(basename(path))[0] + # build astng representation + try: + sys.path.insert(0, dirname(path)) + node = self.string_build(data, modname, path) + node.file = abspath(path) + finally: + self._file = None + sys.path.pop(0) + return node + + def string_build(self, data, modname='', path=None): + """build astng from a source code stream (i.e. from an ast)""" + try: + return self.ast_build(parse(data + '\n'), modname, path) + except ParserError, ex: + # compiler.parse with python <= 2.2 raise ParserError instead of + # SyntaxError + ex = SyntaxError('invalid syntax') + ex.lineno = 1 # dummy line number + raise ex + + def ast_build(self, node, modname=None, path=None): + """recurse on the ast (soon ng) to add some arguments et method + """ + if path is not None: + node.file = node.path = abspath(path) + else: + node.file = node.path = '<?>' + if modname.endswith('.__init__'): + modname = modname[:-9] + node.package = True + else: + node.package = path and path.find('__init__.py') > -1 or False + node.name = modname + node.pure_python = True + self._walker.walk(node) + return node + + # callbacks to build from an existing compiler.ast tree ################### + + def visit_module(self, node): + """visit a stmt.Module node -> init node and push the corresponding + object or None on the top of the stack + """ + self._stack = [self._module] + self._par_stack = [node] + self._metaclass = [''] + self._global_names = [] + node.parent = None + node.globals = node.locals = {} + for name, value in ( ('__name__', node.name), + ('__file__', node.path), + ('__doc__', node.doc) ): + const = nodes.Const(value) + const.parent = node + node.locals[name] = [const] + attach___dict__(node) + if node.package: + # FIXME: List(Const()) + const = nodes.Const(dirname(node.path)) + const.parent = node + node.locals['__path__'] = [const] + + + def leave_module(self, _): + """leave a stmt.Module node -> pop the last item on the stack and check + the stack is empty + """ + self._stack.pop() + assert not self._stack, 'Stack is not empty : %s' % self._stack + self._par_stack.pop() + assert not self._par_stack, \ + 'Parent stack is not empty : %s' % self._par_stack + + def visit_class(self, node): + """visit a stmt.Class node -> init node and push the corresponding + object or None on the top of the stack + """ + self.visit_default(node) + node.instance_attrs = {} + node.basenames = [b_node.as_string() for b_node in node.bases] + self._push(node) + for name, value in ( ('__name__', node.name), + ('__module__', node.root().name), + ('__doc__', node.doc) ): + const = nodes.Const(value) + const.parent = node + node.locals[name] = [const] + attach___dict__(node) + self._metaclass.append(self._metaclass[-1]) + + def leave_class(self, node): + """leave a stmt.Class node -> pop the last item on the stack + """ + self.leave_default(node) + self._stack.pop() + metaclass = self._metaclass.pop() + if not node.bases: + # no base classes, detect new / style old style according to + # current scope + node._newstyle = metaclass == 'type' + + def visit_function(self, node): + """visit a stmt.Function node -> init node and push the corresponding + object or None on the top of the stack + """ + self.visit_default(node) + self._global_names.append({}) + node.argnames = list(node.argnames) + if isinstance(node.parent.frame(), nodes.Class): + node.type = 'method' + if node.name == '__new__': + node.type = 'classmethod' + self._push(node) + register_arguments(node, node.argnames) + + def leave_function(self, node): + """leave a stmt.Function node -> pop the last item on the stack + """ + self.leave_default(node) + self._stack.pop() + self._global_names.pop() + + def visit_lambda(self, node): + """visit a stmt.Lambda node -> init node locals + """ + self.visit_default(node) + node.argnames = list(node.argnames) + node.locals = {} + register_arguments(node, node.argnames) + + def visit_global(self, node): + """visit a stmt.Global node -> add declared names to locals + """ + self.visit_default(node) + if not self._global_names: # global at the module level, no effect + return + for name in node.names: + self._global_names[-1].setdefault(name, []).append(node) +# node.parent.set_local(name, node) +# module = node.root() +# if module is not node.frame(): +# for name in node.names: +# module.set_local(name, node) + + def visit_import(self, node): + """visit a stmt.Import node -> add imported names to locals + """ + self.visit_default(node) + for (name, asname) in node.names: + name = asname or name + node.parent.set_local(name.split('.')[0], node) + + def visit_from(self, node): + """visit a stmt.From node -> add imported names to locals + """ + self.visit_default(node) + # add names imported by the import to locals + for (name, asname) in node.names: + if name == '*': + try: + imported = node.root().import_module(node.modname) + except ASTNGBuildingException: + #import traceback + #traceback.print_exc() + continue + # FIXME: log error + #print >> sys.stderr, \ + # 'Unable to get imported names for %r line %s"' % ( + # node.modname, node.lineno) + for name in imported.wildcard_import_names(): + node.parent.set_local(name, node) + else: + node.parent.set_local(asname or name, node) + + def leave_decorators(self, node): + """python >= 2.4 + visit a stmt.Decorator node -> check for classmethod and staticmethod + """ + func = node.parent + for decorator_expr in node.nodes: + if isinstance(decorator_expr, nodes.Name) and \ + decorator_expr.name in ('classmethod', 'staticmethod'): + func.type = decorator_expr.name + self.leave_default(node) + + def visit_assign(self, node): + """visit a stmt.Assign node -> check for classmethod and staticmethod + + __metaclass__ + """ + self.visit_default(node) + klass = node.parent.frame() + #print node + if isinstance(klass, nodes.Class) and \ + isinstance(node.expr, nodes.CallFunc) and \ + isinstance(node.expr.node, nodes.Name): + func_name = node.expr.node.name + if func_name in ('classmethod', 'staticmethod'): + for ass_node in node.nodes: + if isinstance(ass_node, nodes.AssName): + try: + meth = klass[ass_node.name] + if isinstance(meth, nodes.Function): + meth.type = func_name + #else: + # print >> sys.stderr, 'FIXME 1', meth + except KeyError: + #print >> sys.stderr, 'FIXME 2', ass_node.name + continue + elif (isinstance(node.nodes[0], nodes.AssName) + and node.nodes[0].name == '__metaclass__'): # XXX check more... + self._metaclass[-1] = 'type' + + def visit_assname(self, node): + """visit a stmt.AssName node -> add name to locals + """ + self.visit_default(node) + self._add_local(node, node.name) + + def visit_augassign(self, node): + """visit a stmt.AssName node -> add name to locals + """ + self.visit_default(node) + if not isinstance(node.node, nodes.Name): + return # XXX + self._add_local(node, node.node.name) + + def _add_local(self, node, name): + if self._global_names and name in self._global_names[-1]: + node.root().set_local(name, node) + else: + node.parent.set_local(name, node) + + def visit_assattr(self, node): + """visit a stmt.AssAttr node -> add name to locals, handle members + definition + """ + self.visit_default(node) + frame = node.frame() + if isinstance(frame, nodes.Function) and frame.type != 'function': + klass = frame.parent.frame() + # are we assigning to a (new ?) instance attribute ? + try: + _self = frame.argnames[0] + except IndexError: + # first argument is missing ! + return + if isinstance(node.expr, nodes.Name) and node.expr.name == _self: + # unittest_scoped_nodes.ClassNodeTC.test_classmethod_attributes + # + # if frame.type == 'classmethod': XXX at this point we may have + # not encountered the classmethod decorator, so we havn't yet + # the correct type + # hack according to the argument name + if _self == 'self': + iattrs = klass.instance_attrs + else: + iattrs = klass.locals + # assign if not yet existant in others + if not iattrs.has_key(node.attrname): + iattrs[node.attrname] = [node] + # but always assign in __init__, except if previous assigment + # already come from __init__ + elif frame.name == '__init__' and not \ + iattrs[node.attrname][0].frame().name == '__init__': + iattrs[node.attrname].insert(0, node) + else: + iattrs[node.attrname].append(node) + + def visit_default(self, node): + """default visit method, handle the parent attribute + """ + node.parent = self._par_stack[-1] + assert node.parent is not node + self._par_stack.append(node) + + def leave_default(self, _): + """default leave method, handle the parent attribute + """ + self._par_stack.pop() + + def _push(self, node): + """update the stack and init some parts of the Function or Class node + """ + obj = getattr(self._stack[-1], node.name, None) + self._stack.append(obj) + node.locals = {} + node.parent.frame().set_local(node.name, node) + + # astng from living objects ############################################### + # + # this is actually a really minimal representation, including only Module, + # Function and Class nodes and some others as guessed + + def object_build(self, node, obj): + """recursive method which create a partial ast from real objects + (only function, class, and method are handled) + """ + if self._done.has_key(obj): + return self._done[obj] + self._done[obj] = node + modname = self._module.__name__ + modfile = getattr(self._module, '__file__', None) + for name in dir(obj): + try: + member = getattr(obj, name) + except AttributeError: + # damned ExtensionClass.Base, I know you're there ! + attach_dummy_node(node, name) + continue + if ismethod(member): + member = member.im_func + if isfunction(member): + # verify this is not an imported function + if member.func_code.co_filename != modfile: + attach_dummy_node(node, name) + continue + object_build_function(node, member) + elif isbuiltin(member): + # verify this is not an imported member + if self._member_module(member) != modname: + imported_member(node, member, name) + continue + object_build_methoddescriptor(node, member) + elif isclass(member): + # verify this is not an imported class + if self._member_module(member) != modname: + imported_member(node, member, name) + continue + if member in self._done: + class_node = self._done[member] + node.add_local_node(class_node) + else: + class_node = object_build_class(node, member) + # recursion + self.object_build(class_node, member) + elif ismethoddescriptor(member): + assert isinstance(member, object) + object_build_methoddescriptor(node, member) + elif isdatadescriptor(member): + assert isinstance(member, object) + object_build_datadescriptor(node, member, name) + elif isinstance(member, (int, long, float, str, unicode)) or member is None: + attach_const_node(node, name, member) + else: + # create an empty node so that the name is actually defined + attach_dummy_node(node, name) + + def _member_module(self, member): + modname = getattr(member, '__module__', None) + return self._dyn_modname_map.get(modname, modname) + +def imported_member(node, member, name): + """consider a class/builtin member where __module__ != current module name + + check if it's sound valid and then add an import node, else use a dummy node + """ + # /!\ some classes like ExtensionClass doesn't have a + # __module__ attribute ! + member_module = getattr(member, '__module__', '__builtin__') + try: + getattr(sys.modules[member_module], name) + except (KeyError, AttributeError): + attach_dummy_node(node, name) + else: + attach_import_node(node, member_module, name) + +# optimize the tokenize module +#from logilab.common.bind import optimize_module +#import tokenize +#optimize_module(sys.modules['tokenize'], tokenize.__dict__) +#optimize_module(sys.modules[__name__], sys.modules[__name__].__dict__) diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 00000000..bd29ae6e --- /dev/null +++ b/debian/changelog @@ -0,0 +1,51 @@ +logilab-astng (0.16.0-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault <sylvain.thenault@logilab.fr> Wed, 19 Apr 2006 17:13:42 +0200 + +logilab-astng (0.15.1-1) unstable; urgency=low + + * new upstream release + + -- Sylvain Thénault <sylvain.thenault@logilab.fr> Fri, 10 Mar 2006 10:15:49 +0100 + +logilab-astng (0.15.0-1) unstable; urgency=low + + * new upstream release (closes: #352889) + + -- Sylvain Thénault <sylvain.thenault@logilab.fr> Mon, 6 Mar 2006 09:18:47 +0100 + +logilab-astng (0.14.0-2) unstable; urgency=low + + * removed packaging bug + * upload to Debian + + -- Alexandre Fayolle <afayolle@debian.org> Fri, 10 Feb 2006 15:29:00 +0100 + +logilab-astng (0.14.0-1) unstable; urgency=low + + * new upstream support + * reorganization to install into site-python, removing the need for + pythonX.X- packages + + -- Sylvain Thénault <sylvain.thenault@logilab.fr> Tue, 10 Jan 2006 14:18:38 +0100 + +logilab-astng (0.13.1-2) unstable; urgency=low + + * Dropped python2.2 support, and removed -test package, as suggested by ftpmaster + + -- Alexandre Fayolle <afayolle@debian.org> Mon, 21 Nov 2005 10:14:38 +0100 + +logilab-astng (0.13.1-1) unstable; urgency=low + + * new upstream release (closes: #337960) + + -- Sylvain Thénault <sylvain.thenault@logilab.fr> Mon, 7 Nov 2005 15:38:42 +0100 + +logilab-astng (0.13.0-1) unstable; urgency=low + + * initial upstream/debian release + + -- Sylvain Thénault <sylvain.thenault@logilab.fr> Fri, 21 Oct 2005 15:51:17 +0200 + diff --git a/debian/control b/debian/control new file mode 100644 index 00000000..ad8b8ec0 --- /dev/null +++ b/debian/control @@ -0,0 +1,28 @@ +Source: logilab-astng +Section: python +Priority: optional +Maintainer: Sylvain Thenault <sylvain.thenault@logilab.fr> +Uploader: Alexandre Fayolle <afayolle@debian.org> +Build-Depends: debhelper (>= 4.0.0), python-dev +Standards-Version: 3.6.2 + +Package: python-logilab-astng +Architecture: all +Depends: python, python-logilab-common +Provides: python2.2-logilab-astng, python2.3-logilab-astng, python2.4-logilab-astng +Conflicts: python2.2-logilab-astng, python2.3-logilab-astng, python2.4-logilab-astng +Replaces: python2.2-logilab-astng, python2.3-logilab-astng, python2.4-logilab-astng +Description: extend python's abstract syntax tree + The aim of this module is to provide a common base representation of + python source code for projects such as pychecker, pyreverse, + pylint... Well, actually the development of this library is essentialy + governed by pylint's needs. + . + It extends class defined in the compiler.ast [1] module with some + additional methods and attributes. Instance attributes are added by a + builder object, which can either generate extended ast (let's call + them astng ;) by visiting an existant ast tree or by inspecting living + object. Methods are added by monkey patching ast classes. + . + Homepage: http://www.logilab.org/projects/astng + diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 00000000..f11944e2 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,28 @@ +This package was debianized by Sylvain Thenault <sylvain.thenault@logilab.fr> Sat, 13 Apr 2002 19:05:23 +0200. + +It was downloaded from ftp://ftp.logilab.org/pub/astng + +Upstream Author: + + Sylvain Thenault <sylvain.thenault@logilab.fr> + +Copyright: + +Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). +http://www.logilab.fr/ -- mailto:contact@logilab.fr + +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., +51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA. + +On Debian systems, the complete text of the GNU General Public License +may be found in '/usr/share/common-licenses/GPL'. diff --git a/debian/python-logilab-astng.dirs b/debian/python-logilab-astng.dirs new file mode 100644 index 00000000..ab6d53bb --- /dev/null +++ b/debian/python-logilab-astng.dirs @@ -0,0 +1,6 @@ +usr/lib/site-python +usr/lib/site-python/logilab +usr/lib/site-python/logilab/astng +usr/share/doc/python-logilab-astng +usr/share/doc/python-logilab-astng +usr/share/doc/python-logilab-astng/test diff --git a/debian/python-logilab-astng.postinst b/debian/python-logilab-astng.postinst new file mode 100644 index 00000000..783b00ea --- /dev/null +++ b/debian/python-logilab-astng.postinst @@ -0,0 +1,26 @@ +#! /bin/sh -e +# + + +touch /usr/lib/site-python/logilab/__init__.py + + +# precompile python files +VERSION=2.3 +PACKAGEDIR=/usr/lib/site-python/logilab/astng +case "$1" in + configure|abort-upgrade|abort-remove|abort-deconfigure) + python$VERSION -O /usr/lib/python$VERSION/compileall.py -q $PACKAGEDIR + python$VERSION /usr/lib/python$VERSION/compileall.py -q $PACKAGEDIR + ;; + + *) + echo "postinst called with unknown argument \`$1'" >&2 + exit 1 + ;; +esac + + +#DEBHELPER# + +exit 0 diff --git a/debian/python-logilab-astng.prerm b/debian/python-logilab-astng.prerm new file mode 100644 index 00000000..c4cb31f8 --- /dev/null +++ b/debian/python-logilab-astng.prerm @@ -0,0 +1,14 @@ +#! /bin/sh -e +# + +# remove .pyc and .pyo files +dpkg --listfiles python-logilab-astng | + awk '$0~/\.py$/ {print $0"c\n" $0"o"}' | + xargs rm -f >&2 + + + + +#DEBHELPER# + +exit 0 diff --git a/debian/rules b/debian/rules new file mode 100755 index 00000000..5cc257bc --- /dev/null +++ b/debian/rules @@ -0,0 +1,71 @@ +#!/usr/bin/make -f +# Sample debian/rules that uses debhelper. +# GNU copyright 1997 to 1999 by Joey Hess. +# +# adapted by Logilab for automatic generation by debianize +# (part of the devtools project, http://www.logilab.org/projects/devtools) +# +# Copyright (c) 2003-2005 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr + +# Uncomment this to turn on verbose mode. +#export DH_VERBOSE=1 + +# This is the debhelper compatability version to use. +export DH_COMPAT=4 + + + +build: build-stamp +build-stamp: + dh_testdir + python setup.py -q build + touch build-stamp + +clean: + dh_testdir + dh_testroot + rm -f build-stamp configure-stamp + rm -rf build + find . -name "*.pyc" | xargs rm -f + rm -f changelog.gz + dh_clean + +install: build + dh_testdir + dh_testroot + dh_clean -k + dh_installdirs + python setup.py -q install_lib --no-compile --install-dir=debian/python-logilab-astng/usr/lib/site-python + python setup.py -q install_headers --install-dir=debian/python-logilab-astng/usr/include/ + # remove sub-package __init__ file (created in postinst) + rm debian/python-logilab-astng/usr/lib/site-python/logilab/__init__.py + # remove test directory (installed in a separated package) + rm -rf debian/python-logilab-astng/usr/lib/site-python/logilab/astng/test + # install tests + (cd test && find . -type f -not \( -path '*/CVS/*' -or -name '*.pyc' \) -exec install -D --mode=644 {} ../debian/python-logilab-astng/usr/share/doc/python-logilab-astng/test/{} \;) + + +# Build architecture-independent files here. +binary-indep: build install + dh_testdir + dh_testroot + dh_install -i + gzip -9 -c ChangeLog > changelog.gz + dh_installchangelogs -i + dh_installexamples -i + dh_installdocs -i README changelog.gz + dh_installman -i + dh_link -i + dh_compress -i -X.py -X.ini -X.xml -Xtest + dh_fixperms -i + dh_installdeb -i + dh_gencontrol -i + dh_md5sums -i + dh_builddeb -i + + + +binary: binary-indep +.PHONY: build clean binary binary-indep + diff --git a/debian/watch b/debian/watch new file mode 100644 index 00000000..b86b1270 --- /dev/null +++ b/debian/watch @@ -0,0 +1,3 @@ +version=2 +ftp://ftp.logilab.org/pub/astng/astng-(.*)\.tar\.gz debian uupdate + diff --git a/inference.py b/inference.py new file mode 100644 index 00000000..6fca9e41 --- /dev/null +++ b/inference.py @@ -0,0 +1,388 @@ +# 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. +"""this module contains a set of functions to handle inference on astng trees + +:version: $Revision: 1.25 $ +:author: Sylvain Thenault +:copyright: 2003-2006 LOGILAB S.A. (Paris, FRANCE) +:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org +:copyright: 2003-2006 Sylvain Thenault +:contact: mailto:thenault@gmail.com +""" + +from __future__ import generators + +__revision__ = "$Id: inference.py,v 1.25 2006-04-20 07:37:28 syt Exp $" +__doctype__ = "restructuredtext en" + +from logilab.common.compat import imap + +from logilab.astng import YES, Instance, Generator, \ + unpack_infer, _infer_stmts, nodes +from logilab.astng import InferenceError, UnresolvableName, \ + NoDefault, NotFoundError, ASTNGBuildingException + +def path_wrapper(func): + """return the given infer function wrapped to handle the path""" + def wrapped(node, name=None, path=None, _func=func, **kwargs): + """wrapper function handling path""" + if path is None: + path = [(node, name)] + else: + if (node, name) in path: + raise StopIteration() + path.append( (node, name) ) + #print '--'*len(path),_func.__name__[6:], getattr(path[-1][0], 'name', ''), name + try: + for res in _func(node, name, path, **kwargs): + #print '--'*len(path), _func.__name__[6:], '-->', res + yield res + path.pop() + except: + path.pop() + raise + return wrapped + +# .infer method ############################################################### + +def infer_default(self, name=None, path=None): + """we don't know how to resolve a statement by default""" + #print 'inference error', self, name, path + raise InferenceError(self.__class__.__name__) + +#infer_default = infer_default +nodes.Node.infer = infer_default + + +def infer_end(self, name=None, path=None): + """inference's end for node such as Module, Class, Function, Const... + """ + yield self + +#infer_end = path_wrapper(infer_end) +nodes.Module.infer = nodes.Class.infer = infer_end +nodes.List.infer = infer_end +nodes.Tuple.infer = infer_end +nodes.Dict.infer = infer_end +nodes.Const.infer = infer_end + + +def infer_function(self, name=None, path=None): + """infer on Function nodes must be take with care since it + may be called to infer one of it's argument (in which case <name> + should be given) + """ + # no name is given, we are infering the function itself + if name is None: + yield self + return + # Function.argnames can be None in astng (means that we don't have + # information on argnames), in which case we can't do anything more + if self.argnames is None: + yield YES + return + if not name in self.argnames: + raise InferenceError() + # first argument of instance/class method + if name == self.argnames[0]: + if self.type == 'method': + yield Instance(self.parent.frame()) + return + if self.type == 'classmethod': + yield self.parent.frame() + return + mularg = self.mularg_class(name) + if mularg is not None: # */** argument, no doubt it's a Tuple or Dict + yield mularg + return + # if there is a default value, yield it. And then yield YES to reflect + # we can't guess given argument value + try: + for infered in self.default_value(name).infer(name, path): + yield infered + yield YES + except NoDefault: + yield YES + +nodes.Function.infer = path_wrapper(infer_function) +nodes.Lambda.infer = path_wrapper(infer_function) + + +def infer_name(self, name=None, path=None): + """infer a Name: use name lookup rules""" + frame, stmts = self.lookup(self.name) + if not stmts: + raise UnresolvableName(name) + return _infer_stmts(stmts, self.name, path, frame) + +nodes.Name.infer = path_wrapper(infer_name) + + +def infer_assname(self, name=None, path=None): + """infer a AssName/AssAttr: need to inspect the RHS part of the + assign node + """ + stmts = self.assigned_stmts(inf_path=path) + return _infer_stmts(stmts, self.name, path) + +nodes.AssName.infer = path_wrapper(infer_assname) + + +def infer_assattr(self, name=None, path=None): + """infer a AssName/AssAttr: need to inspect the RHS part of the + assign node + """ + stmts = self.assigned_stmts(inf_path=path) + return _infer_stmts(stmts, self.attrname, path) + +nodes.AssAttr.infer = path_wrapper(infer_assattr) + + +def infer_callfunc(self, name=None, path=None): + """infer a CallFunc node by trying to guess what's the function is + returning + """ + one_infered = False + for callee in self.node.infer(name, path): + if callee is YES: + yield callee + one_infered = True + continue + try: + for infered in callee.infer_call_result(self, path): + yield infered + one_infered = True + except (AttributeError, InferenceError): + ## XXX log error ? + continue + if not one_infered: + raise InferenceError() + +nodes.CallFunc.infer = path_wrapper(infer_callfunc) + + +def infer_getattr(self, name=None, path=None): + """infer a Getattr node by using getattr on the associated object + """ + one_infered = False + for owner in self.expr.infer(name, path): + if owner is YES: + yield owner + one_infered = True + continue + try: + for obj in owner.igetattr(self.attrname, path=path): + yield obj + one_infered = True + except (NotFoundError, InferenceError): + continue + except AttributeError: + # XXX method / function + continue + if not one_infered: + raise InferenceError() + +nodes.Getattr.infer = path_wrapper(infer_getattr) + + +def _imported_module_astng(node, modname): + """return the ast for a module whose name is <modname> imported by <node> + """ + # handle special case where we are on a package node importing a module + # using the same name as the package, which may end in an infinite loop + # on relative imports + # XXX: no more needed ? + mymodule = node.root() + if mymodule.relative_name(modname) == mymodule.name: + # FIXME: I don't know what to do here... + raise InferenceError(modname) + try: + return mymodule.import_module(modname) + except (ASTNGBuildingException, SyntaxError): + raise InferenceError(modname) + +def infer_import(self, name, path=None, asname=True): + """self resolve on From / Import nodes return the imported module/object""" + if name is None: + infer_default(self, name, path) + if asname: + yield _imported_module_astng(self, self.real_name(name)) + else: + yield _imported_module_astng(self, name) + +nodes.Import.infer = path_wrapper(infer_import) + +def infer_from(self, name, path=None, asname=True): + """self resolve on From / Import nodes return the imported module/object""" + if name is None: + infer_default(self, name, path) + module = _imported_module_astng(self, self.modname) + if asname: + name = self.real_name(name) + try: + return _infer_stmts(module.getattr(name), name, path) + except NotFoundError: + raise InferenceError(name) + +nodes.From.infer = path_wrapper(infer_from) + + +def infer_global(self, name=None, path=None): + try: + return _infer_stmts(self.root().getattr(name), name, path) + except NotFoundError: + raise InferenceError() +nodes.Global.infer = path_wrapper(infer_global) + +# .infer_call_result method ################################################### +def callable_default(self): + return False +nodes.Node.callable = callable_default +def callable_true(self): + return True +nodes.Function.callable = callable_true +nodes.Lambda.callable = callable_true +nodes.Class.callable = callable_true + +def infer_call_result_function(self, caller, inf_path=None): + """infer what's a function is returning wen called""" + if self.is_generator(): + yield Generator(self) + return + returns = self.nodes_of_class(nodes.Return, skip_klass=nodes.Function) + #for infered in _infer_stmts(imap(lambda n:n.value, returns), path=inf_path): + # yield infered + for returnnode in returns: + try: + for infered in returnnode.value.infer(path=inf_path): + yield infered + except InferenceError: + yield YES +nodes.Function.infer_call_result = infer_call_result_function + +def infer_call_result_class(self, caller, inf_path=None): + """infer what's a class is returning when called""" + yield Instance(self) + +nodes.Class.infer_call_result = infer_call_result_class + + +# Assignment related nodes #################################################### + +def assend_assigned_stmts(self, inf_path=None): + # only infer *real* assignments + if self.flags == 'OP_DELETE': + raise InferenceError() + return self.parent.assigned_stmts(self, inf_path=inf_path) + +nodes.AssName.assigned_stmts = assend_assigned_stmts +nodes.AssAttr.assigned_stmts = assend_assigned_stmts + +def mulass_assigned_stmts(self, node, path=None, inf_path=None): + if path is None: + path = [] + node_idx = self.nodes.index(node) + path.insert(0, node_idx) + return self.parent.assigned_stmts(self, path, inf_path) +nodes.AssTuple.assigned_stmts = mulass_assigned_stmts +nodes.AssList.assigned_stmts = mulass_assigned_stmts + +def assign_assigned_stmts(self, node, path=None, inf_path=None): + """WARNING here `path` is a list of index to follow""" + if not path: + yield self.expr + return + found = False + for infered in _resolve_asspart(self.expr.infer(path=inf_path), path, inf_path): + found = True + yield infered + if not found: + raise InferenceError() + +nodes.Assign.assigned_stmts = assign_assigned_stmts + +def _resolve_asspart(parts, asspath, path): + """recursive function to resolve multiple assignments""" + asspath = asspath[:] + index = asspath.pop(0) + for part in parts: + try: + assigned = part.getitem(index) + except (AttributeError, IndexError): + return + if not asspath: + # we acheived to resolved the assigment path, + # don't infer the last part + found = True + yield assigned + elif assigned is YES: + return + else: + # we are not yet on the last part of the path + # search on each possibly infered value + try: + for infered in _resolve_asspart(assigned.infer(path=path), asspath, path): + yield infered + except InferenceError: + return + +def tryexcept_assigned_stmts(self, node, path=None, inf_path=None): + found = False + for exc_type, exc_obj, body in self.handlers: + if node is exc_obj: + for assigned in unpack_infer(exc_type): + if isinstance(assigned, nodes.Class): + assigned = Instance(assigned) + yield assigned + found = True + break + if not found: + raise InferenceError() +nodes.TryExcept.assigned_stmts = tryexcept_assigned_stmts + +def XXX_assigned_stmts(self, node, path=None, inf_path=None): + raise InferenceError() +nodes.For.assigned_stmts = XXX_assigned_stmts +nodes.ListCompFor.assigned_stmts = XXX_assigned_stmts +nodes.GenExprFor.assigned_stmts = XXX_assigned_stmts + +def end_ass_type(self): + return self +nodes.For.ass_type = end_ass_type +nodes.ListCompFor.ass_type = end_ass_type +nodes.GenExprFor.ass_type = end_ass_type +nodes.TryExcept.ass_type = end_ass_type +nodes.Assign.ass_type = end_ass_type +nodes.AugAssign.ass_type = end_ass_type +def parent_ass_type(self): + return self.parent.ass_type() +nodes.AssName.ass_type = parent_ass_type +nodes.AssAttr.ass_type = parent_ass_type +nodes.AssTuple.ass_type = parent_ass_type +nodes.AssList.ass_type = parent_ass_type +def assend_ass_type(self, inf_path=None): + # only infer *real* assignments + if self.flags == 'OP_DELETE': + return self + return self.parent.ass_type() +nodes.AssName.ass_type = assend_ass_type +nodes.AssAttr.ass_type = assend_ass_type + +# subscription protocol ####################################################### + +def getitem(self, index): + return self.nodes[index] +nodes.List.getitem = getitem +nodes.Tuple.getitem = getitem +#Dict.getitem = getitem XXX + diff --git a/inspector.py b/inspector.py new file mode 100644 index 00000000..9693e4d3 --- /dev/null +++ b/inspector.py @@ -0,0 +1,267 @@ +# 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. +"""visitor doing some postprocessing on the astng tree. +Try to resolve definitions (namespace) dictionnary, relationship... + +This module has been imported from pyreverse + + +:version: $Revision: 1.6 $ +:author: Sylvain Thenault +:copyright: 2003-2005 LOGILAB S.A. (Paris, FRANCE) +:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org +:copyright: 2003-2005 Sylvain Thenault +:contact: mailto:thenault@gmail.com +""" + +__revision__ = "$Id: inspector.py,v 1.6 2006-01-24 19:52:07 syt Exp $" +__doctype__ = "restructuredtext en" + +from os.path import dirname + +from logilab.common.modutils import get_module_part, is_relative, \ + is_standard_module + +from logilab import astng +from logilab.astng.utils import LocalsVisitor + +class IdGeneratorMixIn: + """ + Mixin adding the ability to generate integer uid + """ + def __init__(self, start_value=0): + self.id_count = start_value + + def init_counter(self, start_value=0): + """init the id counter + """ + self.id_count = start_value + + def generate_id(self): + """generate a new identifer + """ + self.id_count += 1 + return self.id_count + + +class Linker(IdGeneratorMixIn, LocalsVisitor): + """ + walk on the project tree and resolve relationships. + + According to options the following attributes may be added to visited nodes: + + * uid, + a unique identifier for the node (on astng.Project, astng.Module, + astng.Class and astng.locals_type). Only if the linker has been instantiad + with tag=True parameter (False by default). + + * Function + a mapping from locals'names to their bounded value, which may be a + constant like a string or an integer, or an astng node (on astng.Module, + astng.Class and astng.Function). + + * instance_attrs_type + as locals_type but for klass member attributes (only on astng.Class) + + * implements, + list of implemented interfaces _objects_ (only on astng.Class nodes) + """ + + def __init__(self, project, inherited_interfaces=0, tag=False): + IdGeneratorMixIn.__init__(self) + LocalsVisitor.__init__(self) + # take inherited interface in consideration or not + self.inherited_interfaces = inherited_interfaces + # tag nodes or not + self.tag = tag + # visited project + self.project = project + + + def visit_project(self, node): + """visit an astng.Project node + + * optionaly tag the node wth a unique id + """ + if self.tag: + node.uid = self.generate_id() + for module in node.modules: + self.visit(module) + + def visit_package(self, node): + """visit an astng.Package node + + * optionaly tag the node wth a unique id + """ + if self.tag: + node.uid = self.generate_id() + for subelmt in node.values(): + self.visit(subelmt) + + def visit_module(self, node): + """visit an astng.Module node + + * set the locals_type mapping + * set the depends mapping + * optionaly tag the node wth a unique id + """ + if hasattr(node, 'locals_type'): + return + node.locals_type = {} + node.depends = [] + if self.tag: + node.uid = self.generate_id() + + def visit_class(self, node): + """visit an astng.Class node + + * set the locals_type and instance_attrs_type mappings + * set the implements list and build it + * optionaly tag the node wth a unique id + """ + if hasattr(node, 'locals_type'): + return + node.locals_type = {} + if self.tag: + node.uid = self.generate_id() + # resolve ancestors + for baseobj in node.ancestors(recurs=False): + specializations = getattr(baseobj, 'specializations', []) + specializations.append(node) + baseobj.specializations = specializations + # resolve instance attributes + node.instance_attrs_type = {} + for assattrs in node.instance_attrs.values(): + for assattr in assattrs: + self.visit_assattr(assattr, node) + # resolve implemented interface + try: + node.implements = list(node.interfaces(self.inherited_interfaces)) + except TypeError: + node.implements = () + + def visit_function(self, node): + """visit an astng.Function node + + * set the locals_type mapping + * optionaly tag the node wth a unique id + """ + if hasattr(node, 'locals_type'): + return + node.locals_type = {} + if self.tag: + node.uid = self.generate_id() + + link_project = visit_project + link_module = visit_module + link_class = visit_class + link_function = visit_function + + def visit_assname(self, node): + """visit an astng.AssName node + + handle locals_type + """ + frame = node.frame() + try: + values = list(node.infer()) + try: + already_infered = frame.locals_type[node.name] + for valnode in values: + if not valnode in already_infered: + already_infered.append(valnode) + except KeyError: + frame.locals_type[node.name] = values + except astng.InferenceError: + pass + + def visit_assattr(self, node, parent): + """visit an astng.AssAttr node + + handle instance_attrs_type + """ + try: + values = list(node.infer()) + try: + already_infered = parent.instance_attrs_type[node.attrname] + for valnode in values: + if not valnode in already_infered: + already_infered.append(valnode) + except KeyError: + parent.instance_attrs_type[node.attrname] = values + except astng.InferenceError: + pass + + def visit_import(self, node): + """visit an astng.Import node + + resolve module dependencies + """ + context_file = node.root().file + for name in node.names: + relative = is_relative(name[0], context_file) + self._imported_module(node, name[0], relative) + + + def visit_from(self, node): + """visit an astng.From node + + resolve module dependencies + """ + basename = node.modname + context_file = node.root().file + if context_file is not None: + relative = is_relative(basename, context_file) + else: + relative = False + for name in node.names: + if name[0] == '*': + continue + # analyze dependancies + fullname = '%s.%s' % (basename, name[0]) + if fullname.find('.') > -1: + try: + # XXX: don't use get_module_part, missing package precedence + fullname = get_module_part(fullname) + except ImportError: + continue + if fullname != basename: + self._imported_module(node, fullname, relative) + + + def compute_module(self, context_name, mod_path): + """return true if the module should be added to dependencies""" + package_dir = dirname(self.project.path) + if context_name == mod_path: + return 0 + elif is_standard_module(mod_path, (package_dir,)): + return 1 + return 0 + + # protected methods ######################################################## + + def _imported_module(self, node, mod_path, relative): + """notify an imported module, used to analyze dependancies + """ + module = node.root() + context_name = module.name + if relative: + mod_path = '%s.%s' % ('.'.join(context_name.split('.')[:-1]), + mod_path) + if self.compute_module(context_name, mod_path): + # handle dependancies + if not hasattr(module, 'depends'): + module.depends = [] + mod_paths = module.depends + if not mod_path in mod_paths: + mod_paths.append(mod_path) diff --git a/lookup.py b/lookup.py new file mode 100644 index 00000000..3a9c35c1 --- /dev/null +++ b/lookup.py @@ -0,0 +1,215 @@ +# 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. +"""name lookup methods, available on Name ans scoped (Module, Class, +Function...) nodes: + +* .lookup(name) +* .ilookup(name) + +Be careful, lookup is kinda internal and return a tuple (frame, [stmts]), while +ilookup return an iterator on infered values + +:version: $Revision: 1.6 $ +:author: Sylvain Thenault +:copyright: 2003-2006 LOGILAB S.A. (Paris, FRANCE) +:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org +:copyright: 2003-2006 Sylvain Thenault +:contact: mailto:thenault@gmail.com +""" + +from __future__ import generators + +__revision__ = "$Id: lookup.py,v 1.6 2006-03-06 08:57:53 syt Exp $" +__doctype__ = "restructuredtext en" + +import __builtin__ + +from logilab.astng.utils import are_exclusive +from logilab.astng import MANAGER, _infer_stmts + + +def lookup(self, name): + """lookup a variable name + + return the frame and the list of assignments associated to the given name + according to the scope where it has been found (locals, globals or builtin) + + The lookup is starting from self's frame. If self is not a frame itself and + the name is found in the inner frame locals, statements will be filtered + to remove ignorable statements according to self's location + """ + #assert ID_RGX.match(name), '%r is not a valid identifier' % name + frame = self.frame() + offset = 0 + # adjust frame for class'ancestors and function"s arguments + if isinstance(frame, Class): + if self in frame.bases: + #print 'frame swaping' + frame = frame.parent.frame() + # line offset to avoid that class A(A) resolve the ancestor to + # the defined class + offset = -1 + elif isinstance(frame, (Function, Lambda)): + if self in frame.defaults: + frame = frame.parent.frame() + # line offset to avoid that def func(f=func) resolve the default + # value to the defined function + offset = -1 + #print 'lookup', self.__class__, getattr(self, 'name', 'noname'), name + # resolve name into locals scope + try: + stmts = self._filter_stmts(frame.locals[name], frame, offset) + except KeyError: + stmts = [] + # lookup name into globals if we were not already at the module level + if not stmts and frame.parent is not None: + try: + frame = frame.root() + stmts = self._filter_stmts(frame.locals[name], frame, 0) + #stmts = frame.locals[name] + except KeyError: + pass + # lookup name into builtins + if not stmts: + frame, stmts = builtin_lookup(name) + #print 'return', name, frame.name, [(stmt.__class__.__name__, getattr(stmt, 'name', '???')) for stmt in stmts] + return frame, stmts + +def builtin_lookup(name): + """lookup a name into the builtin module + return the list of matching statements and the astng for the builtin + module + """ + builtinastng = MANAGER.astng_from_module(__builtin__) + try: + stmts = builtinastng.locals[name] + except KeyError: + stmts = () + return builtinastng, stmts + +def ilookup(self, name, path=None): + """infered lookup + + return an iterator on infered values of the statements returned by + the lookup method + """ + frame, stmts = self.lookup(name) + return _infer_stmts(stmts, name, path, frame) + + +def _filter_stmts(self, stmts, frame, offset): + """filter statements: + + If self is not a frame itself and the name is found in the inner + frame locals, statements will be filtered to remove ignorable + statements according to self's location + """ + # if offset == -1, my actual frame is not the inner frame but its parent + # + # class A(B): pass + # + # we need this to resolve B correctly + if offset == -1: + myframe = self.frame().parent.frame() + else: + myframe = self.frame() + if not myframe is frame or self is frame: + return stmts + #print self.name, frame.name + mystmt = self.statement() + # line filtering if we are in the same frame + if myframe is frame: + mylineno = mystmt.source_line() + offset + else: + # disabling lineno filtering + print 'disabling lineno filtering' + mylineno = 0 + _stmts = [] + _stmt_parents = [] + #print '-'*60 + #print 'filtering', stmts, mylineno + for node in stmts: + stmt = node.statement() + # line filtering is on and we have reached our location, break + if mylineno > 0 and stmt.source_line() > mylineno: + #print 'break', mylineno, stmt.source_line() + break + if isinstance(node, Class) and self in node.bases: + #print 'breaking on', self, node.bases + break + try: + ass_type = node.ass_type() + if ass_type is mystmt: + if not isinstance(ass_type, (ListCompFor, GenExprFor)): + #print 'break now2', self, ass_type + break + #print list(ass_type.assigned_stmts()) + #if ass_type.assigned_stmts() + if isinstance(self, (Const, Name)): + _stmts = [self] + #print 'break now', ass_type, self, node + break + except AttributeError: + ass_type = None + # a loop assigment is hidding previous assigment + if isinstance(ass_type, (For, ListCompFor, GenExprFor)) and \ + ass_type.parent_of(self): + _stmts = [node] + _stmt_parents = [stmt.parent] + continue + try: + pindex = _stmt_parents.index(stmt.parent) + except ValueError: + pass + else: + try: + if ass_type and _stmts[pindex].ass_type().parent_of(ass_type): + # print 'skipping', node, node.source_line() + continue + except AttributeError: + pass # name from Import, Function, Class... + if not are_exclusive(self, node): + ###print 'PARENT', stmt.parent + #print 'removing', _stmts[pindex] + del _stmt_parents[pindex] + del _stmts[pindex] + if isinstance(node, AssName): + if stmt.parent is mystmt.parent: + #print 'assign clear' + _stmts = [] + _stmt_parents = [] + if node.flags == 'OP_DELETE': + #print 'delete clear' + _stmts = [] + _stmt_parents = [] + continue + + if not are_exclusive(self, node): + #print 'append', node, node.source_line() + _stmts.append(node) + _stmt_parents.append(stmt.parent) + #print '->', _stmts + stmts = _stmts + return stmts + +def _decorate(astmodule): + """add this module functionalities to necessary nodes""" + for klass in (astmodule.Name, astmodule.Module, astmodule.Class, + astmodule.Function, astmodule.Lambda): + klass.ilookup = ilookup + klass.lookup = lookup + klass._filter_stmts = _filter_stmts + for name in ('Class', 'Function', 'Lambda', + 'For', 'ListCompFor', 'GenExprFor', + 'AssName', 'Name', 'Const'): + globals()[name] = getattr(astmodule, name) diff --git a/manager.py b/manager.py new file mode 100644 index 00000000..49bd514f --- /dev/null +++ b/manager.py @@ -0,0 +1,345 @@ +# 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) + +:version: $Revision: 1.49 $ +:author: Sylvain Thenault +:copyright: 2003-2005 LOGILAB S.A. (Paris, FRANCE) +:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org +:copyright: 2003-2005 Sylvain Thenault +:contact: mailto:thenault@gmail.com +""" + +__revision__ = "$Id: manager.py,v 1.49 2006-03-08 15:52:30 syt Exp $" +__doctype__ = "restructuredtext en" + +import sys +import os +from os.path import dirname, basename, abspath, join, isdir, exists + +from logilab.common.cache import Cache +from logilab.common.modutils import NoSourceFile, is_python_source, \ + file_from_modpath, load_module_from_name, \ + get_module_files, get_source_file +from logilab.common.configuration import OptionsProviderMixIn + +from logilab.astng._exceptions import ASTNGBuildingException + +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() + + +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' : "<file>", + 'dest' : "black_list", "default" : ('CVS',), + 'help' : "add <file> (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' : '<project name>', + 'help' : 'set the project name.'}), + ) + brain = {} + def __init__(self): + self.__dict__ = ASTNGManager.brain + if not self.__dict__: + OptionsProviderMixIn.__init__(self) + self._cache = None + self._mod_file_cache = None + self.set_cache_size(200) + + 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().file_build(filepath, modname) + except SyntaxError: + raise + except Exception, ex: + #if __debug__: + # 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 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().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' % klass) + modastng = self.astng_from_module_name(modname) + return modastng.getattr(klass.__name__)[0] # XXX + + 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.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 getChildNodes(self): + return self.modules + + def __repr__(self): + return '<Project %r at %s (%s modules)>' % (self.name, id(self), + len(self.modules)) diff --git a/nodes.py b/nodes.py new file mode 100644 index 00000000..f31be02a --- /dev/null +++ b/nodes.py @@ -0,0 +1,768 @@ +# 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. +""" +on all nodes : + .is_statement(), returning true if the node should be considered as a + statement node + .root(), returning the root node of the tree (i.e. a Module) + .previous_sibling(), returning previous sibling statement node + .next_sibling(), returning next sibling statement node + .statement(), returning the first parent node marked as statement node + .frame(), returning the first node defining a new local scope (i.e. + Module, Function or Class) + .set_local(name, node), define an identifier <name> on the first parent frame, + with the node defining it. This is used by the astng builder and should not + be used from out there. + .as_string(), returning a string representation of the code (should be + executable). + +on From and Import : + .real_name(name), + + [1] http://docs.python.org/lib/module-compiler.ast.html + +:version: $Revision: 1.103 $ +:author: Sylvain Thenault +:copyright: 2003-2006 LOGILAB S.A. (Paris, FRANCE) +:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org +:copyright: 2003-2006 Sylvain Thenault +:contact: mailto:thenault@gmail.com +""" + +from __future__ import generators + +__revision__ = "$Id: nodes.py,v 1.103 2006-04-19 14:31:38 syt Exp $" +__doctype__ = "restructuredtext en" + +from compiler.ast import Assign, Add, And, AssAttr, AssList, AssName, \ + AssTuple, Assert, Assign, AugAssign, \ + Backquote, Bitand, Bitor, Bitxor, Break, CallFunc, Class, \ + Compare, Const, Continue, Dict, Discard, Div, FloorDiv, \ + Ellipsis, EmptyNode, Exec, \ + For, From, Function, Getattr, Global, \ + If, Import, Invert, Keyword, Lambda, LeftShift, \ + List, ListComp, ListCompFor, ListCompIf, Mod, Module, Mul, Name, Node, \ + Not, Or, Pass, Power, Print, Printnl, Raise, Return, RightShift, Slice, \ + Sliceobj, Stmt, Sub, Subscript, TryExcept, TryFinally, Tuple, UnaryAdd, \ + UnarySub, While, Yield +try: + from compiler.ast import GenExpr, GenExprFor, GenExprIf, GenExprInner +except: + class GenExpr: + """dummy GenExpr node, shouldn't be used since py < 2.4""" + class GenExprFor: + """dummy GenExprFor node, shouldn't be used since py < 2.4""" + class GenExprIf: + """dummy GenExprIf node, shouldn't be used since py < 2.4""" + class GenExprInner: + """dummy GenExprInner node, shouldn't be used since py < 2.4""" + +from logilab.astng._exceptions import NotFoundError, InferenceError +from logilab.astng.utils import extend_class + +import re +ID_RGX = re.compile('^[a-zA-Z_][a-zA-Z_0-9]*$') +del re + +INFER_NEED_NAME_STMTS = (From, Import, Global, TryExcept) + +# Node ###################################################################### + +class NodeNG: + """/!\ this class should not be used directly /!\ it's + only used as a methods and attribute container, and update the + original class from the compiler.ast module using its dictionnary + (see below the class definition) + """ + + # attributes below are set by the builder module or by raw factories + + # parent node in the tree + parent = None + + def __str__(self): + return '%s(%s)' % (self.__class__.__name__, getattr(self, 'name', '')) + + def parent_of(self, node): + """return true if i'm a parent of the given node""" + parent = node.parent + while parent is not None: + if self is parent: + return True + parent = parent.parent + return False + + def is_statement(self): + """return true if the node should be considered as statement node + """ + if isinstance(self.parent, Stmt): + return self + return None + + def statement(self): + """return the first parent node marked as statement node + """ + if self.is_statement(): + return self + return self.parent.statement() + + def frame(self): + """return the first node defining a new local scope (i.e. Module, + Function, Lambda or Class) + """ + return self.parent.frame() + + def root(self): + """return the root node of the tree, (i.e. a Module) + """ + if self.parent: + return self.parent.root() + return self + + def next_sibling(self): + """return the previous sibling statement + """ + while not self.is_statement(): + self = self.parent + index = self.parent.nodes.index(self) + try: + return self.parent.nodes[index+1] + except IndexError: + return + + def previous_sibling(self): + """return the next sibling statement + """ + while not self.is_statement(): + self = self.parent + index = self.parent.nodes.index(self) + if index > 0: + return self.parent.nodes[index-1] + return + + def nearest(self, nodes): + """return the node which is the nearest before this one in the + given list of nodes + """ + myroot = self.root() + mylineno = self.source_line() + nearest = None, 0 + for node in nodes: + assert node.root() is myroot, \ + 'not from the same module %s' % (self, node) + lineno = node.source_line() + if node.source_line() > mylineno: + break + if lineno > nearest[1]: + nearest = node, lineno + # FIXME: raise an exception if nearest is None ? + return nearest[0] + + def source_line(self): + """return the line number where the given node appears + + we need this method since not all nodes as the lineno attribute + correctly set... + """ + line = self.lineno + if line is None: + _node = self + try: + while line is None: + _node = _node.getChildNodes()[0] + line = _node.lineno + except IndexError: + _node = self.parent + while _node and line is None: + line = _node.lineno + _node = _node.parent + self.lineno = line + return line + + def last_source_line(self): + """return the last block line number for this node (i.e. including + children) + """ + try: + return self.__dict__['_cached_last_source_line'] + except KeyError: + line = self.source_line() + for node in self.getChildNodes(): + line = max(line, node.last_source_line()) + self._cached_last_source_line = line + return line + + def block_range(self, lineno): + """handle block line numbers range for non block opening statements + """ + return lineno, self.last_source_line() + + + def set_local(self, name, stmt): + """delegate to a scoped parent handling a locals dictionary + """ + self.parent.set_local(name, stmt) + + def nodes_of_class(self, klass, skip_klass=None): + """return an iterator on nodes which are instance of the given class(es) + + klass may be a class object or a tuple of class objects + """ + if isinstance(self, klass): + yield self + for child_node in self.getChildNodes(): + if skip_klass is not None and isinstance(child_node, skip_klass): + continue + for matching in child_node.nodes_of_class(klass): + yield matching + + def _infer_name(self, frame, name): + if isinstance(self, INFER_NEED_NAME_STMTS) or ( + isinstance(self, (Function, Lambda)) and self is frame): + return name + return None + + +extend_class(Node, NodeNG) + +# block range overrides ####################################################### + +def function_block_range(node, lineno): + """handle block line numbers range for function statements + """ + return node.source_line(), node.last_source_line() + +Function.block_range = function_block_range + +def if_block_range(node, lineno): + """handle block line numbers range for if/elif statements + """ + last = None + for test, testbody in node.tests[1:]: + if lineno == testbody.source_line(): + return lineno, lineno + if lineno <= testbody.last_source_line(): + return lineno, testbody.last_source_line() + if last is None: + last = testbody.source_line() - 1 + return elsed_block_range(node, lineno, last) + +If.block_range = if_block_range + +def try_except_block_range(node, lineno): + """handle block line numbers range for try/except statements + """ + last = None + for excls, exinst, exbody in node.handlers: + if excls and lineno == excls.source_line(): + return lineno, lineno + if exbody.source_line() <= lineno <= exbody.last_source_line(): + return lineno, exbody.last_source_line() + if last is None: + last = exbody.source_line() - 1 + return elsed_block_range(node, lineno, last) + +TryExcept.block_range = try_except_block_range + +def elsed_block_range(node, lineno, last=None): + """handle block line numbers range for try/finally, for and while + statements + """ + if lineno == node.source_line(): + return lineno, lineno + if node.else_: + if lineno >= node.else_.source_line(): + return lineno, node.else_.last_source_line() + return lineno, node.else_.source_line() - 1 + return lineno, last or node.last_source_line() + +TryFinally.block_range = elsed_block_range +While.block_range = elsed_block_range +For.block_range = elsed_block_range + +# From and Import ############################################################# + +def real_name(node, asname): + """get name from 'as' name + """ + for index in range(len(node.names)): + name, _asname = node.names[index] + if name == '*': + return asname + if not _asname: + name = name.split('.', 1)[0] + _asname = name + if asname == _asname: + return name + raise NotFoundError(asname) + +From.real_name = real_name +Import.real_name = real_name + + +# as_string ################################################################### + +def add_as_string(node): + """return an ast.Add node as string""" + return '(%s) + (%s)' % (node.left.as_string(), node.right.as_string()) +Add.as_string = add_as_string + +def and_as_string(node): + """return an ast.And node as string""" + return ' and '.join(['(%s)' % n.as_string() for n in node.nodes]) +And.as_string = and_as_string + +def assattr_as_string(node): + """return an ast.AssAttr node as string""" + if node.flags == 'OP_DELETE': + return 'del %s.%s' % (node.expr.as_string(), node.attrname) + return '%s.%s' % (node.expr.as_string(), node.attrname) +AssAttr.as_string = assattr_as_string + +def asslist_as_string(node): + """return an ast.AssList node as string""" + string = ', '.join([n.as_string() for n in node.nodes]) + return '[%s]' % string +AssList.as_string = asslist_as_string + +def assname_as_string(node): + """return an ast.AssName node as string""" + if node.flags == 'OP_DELETE': + return 'del %s' % node.name + return node.name +AssName.as_string = assname_as_string + +def asstuple_as_string(node): + """return an ast.AssTuple node as string""" + string = ', '.join([n.as_string() for n in node.nodes]) + # fix for del statement + return string.replace(', del ', ', ') +AssTuple.as_string = asstuple_as_string + +def assert_as_string(node): + """return an ast.Assert node as string""" + if node.fail: + return 'assert %s, %s' % (node.test.as_string(), node.fail.as_string()) + return 'assert %s' % node.test.as_string() +Assert.as_string = assert_as_string + +def assign_as_string(node): + """return an ast.Assign node as string""" + lhs = ' = '.join([n.as_string() for n in node.nodes]) + return '%s = %s' % (lhs, node.expr.as_string()) +Assign.as_string = assign_as_string + +def augassign_as_string(node): + """return an ast.AugAssign node as string""" + return '%s %s %s' % (node.node.as_string(), node.op, node.expr.as_string()) +AugAssign.as_string = augassign_as_string + +def backquote_as_string(node): + """return an ast.Backquote node as string""" + return '`%s`' % node.expr.as_string() +Backquote.as_string = backquote_as_string + +def bitand_as_string(node): + """return an ast.Bitand node as string""" + return ' & '.join(['(%s)' % n.as_string() for n in node.nodes]) +Bitand.as_string = bitand_as_string + +def bitor_as_string(node): + """return an ast.Bitor node as string""" + return ' | '.join(['(%s)' % n.as_string() for n in node.nodes]) +Bitor.as_string = bitor_as_string + +def bitxor_as_string(node): + """return an ast.Bitxor node as string""" + return ' ^ '.join(['(%s)' % n.as_string() for n in node.nodes]) +Bitxor.as_string = bitxor_as_string + +def break_as_string(node): + """return an ast.Break node as string""" + return 'break' +Break.as_string = break_as_string + +def callfunc_as_string(node): + """return an ast.CallFunc node as string""" + expr_str = node.node.as_string() + args = ', '.join([arg.as_string() for arg in node.args]) + if node.star_args: + args += ', *%s' % node.star_args.as_string() + if node.dstar_args: + args += ', **%s' % node.dstar_args.as_string() + return '%s(%s)' % (expr_str, args) +CallFunc.as_string = callfunc_as_string + +def class_as_string(node): + """return an ast.Class node as string""" + bases = ', '.join([n.as_string() for n in node.bases]) + bases = bases and '(%s)' % bases or '' + docs = node.doc and '\n """%s"""' % node.doc or '' + return 'class %s%s:%s\n %s\n' % (node.name, bases, docs, + node.code.as_string()) +Class.as_string = class_as_string + +def compare_as_string(node): + """return an ast.Compare node as string""" + rhs_str = ' '.join(['%s %s' % (op, expr.as_string()) + for op, expr in node.ops]) + return '%s %s' % (node.expr.as_string(), rhs_str) +Compare.as_string = compare_as_string + +def const_as_string(node): + """return an ast.Const node as string""" + return repr(node.value) +Const.as_string = const_as_string + +def continue_as_string(node): + """return an ast.Continue node as string""" + return 'continue' +Continue.as_string = continue_as_string + +def dict_as_string(node): + """return an ast.Dict node as string""" + return '{%s}' % ', '.join(['%s: %s' % (key.as_string(), value.as_string()) + for key, value in node.items]) +Dict.as_string = dict_as_string + +def discard_as_string(node): + """return an ast.Discard node as string""" + return node.expr.as_string() +Discard.as_string = discard_as_string + +def div_as_string(node): + """return an ast.Div node as string""" + return '(%s) / (%s)' % (node.left.as_string(), node.right.as_string()) +Div.as_string = div_as_string + +def floordiv_as_string(node): + """return an ast.Div node as string""" + return '(%s) // (%s)' % (node.left.as_string(), node.right.as_string()) +FloorDiv.as_string = floordiv_as_string + +def ellipsis_as_string(node): + """return an ast.Ellipsis node as string""" + return '...' +Ellipsis.as_string = ellipsis_as_string + +def empty_as_string(node): + return '' +EmptyNode.as_string = empty_as_string + +def exec_as_string(node): + """return an ast.Exec node as string""" + if node.globals: + return 'exec %s in %s, %s' % (node.expr.as_string(), + node.locals.as_string(), + node.globals.as_string()) + if node.locals: + return 'exec %s in %s' % (node.expr.as_string(), + node.locals.as_string()) + return 'exec %s' % node.expr.as_string() +Exec.as_string = exec_as_string + +def for_as_string(node): + """return an ast.For node as string""" + fors = 'for %s in %s:\n %s' % (node.assign.as_string(), + node.list.as_string(), + node.body.as_string()) + if node.else_: + fors = '%s\nelse:\n %s' % (fors, node.else_.as_string()) + return fors +For.as_string = for_as_string + +def from_as_string(node): + """return an ast.From node as string""" + return 'from %s import %s' % (node.modname, _import_string(node.names)) +From.as_string = from_as_string + +def function_as_string(node): + """return an ast.Function node as string""" + fargs = node.format_args() + docs = node.doc and '\n """%s"""' % node.doc or '' + return 'def %s(%s):%s\n %s' % (node.name, fargs, docs, + node.code.as_string()) +Function.as_string = function_as_string + +def genexpr_as_string(node): + """return an ast.GenExpr node as string""" + return '(%s)' % node.code.as_string() +GenExpr.as_string = genexpr_as_string + +def genexprinner_as_string(node): + """return an ast.GenExpr node as string""" + return '%s %s' % (node.expr.as_string(), ' '.join([n.as_string() + for n in node.quals])) +GenExprInner.as_string = genexprinner_as_string + +def genexprfor_as_string(node): + """return an ast.GenExprFor node as string""" + return 'for %s in %s %s' % (node.assign.as_string(), + node.iter.as_string(), + ' '.join([n.as_string() for n in node.ifs])) +GenExprFor.as_string = genexprfor_as_string + +def genexprif_as_string(node): + """return an ast.GenExprIf node as string""" + return 'if %s' % node.test.as_string() +GenExprIf.as_string = genexprif_as_string + +def getattr_as_string(node): + """return an ast.Getattr node as string""" + return '%s.%s' % (node.expr.as_string(), node.attrname) +Getattr.as_string = getattr_as_string + +def global_as_string(node): + """return an ast.Global node as string""" + return 'global %s' % ', '.join(node.names) +Global.as_string = global_as_string + +def if_as_string(node): + """return an ast.If node as string""" + cond, body = node.tests[0] + ifs = ['if %s:\n %s' % (cond.as_string(), body.as_string())] + for cond, body in node.tests[1:]: + ifs.append('elif %s:\n %s' % (cond.as_string(), body.as_string())) + if node.else_: + ifs.append('else:\n %s' % node.else_.as_string()) + return '\n'.join(ifs) +If.as_string = if_as_string + +def import_as_string(node): + """return an ast.Import node as string""" + return 'import %s' % _import_string(node.names) +Import.as_string = import_as_string + +def invert_as_string(node): + """return an ast.Invert node as string""" + return '~%s' % node.expr.as_string() +Invert.as_string = invert_as_string + +def keyword_as_string(node): + """return an ast.Keyword node as string""" + return '%s=%s' % (node.name, node.expr.as_string()) +Keyword.as_string = keyword_as_string + +def lambda_as_string(node): + """return an ast.Lambda node as string""" + return 'lambda %s: %s' % (node.format_args(), node.code.as_string()) +Lambda.as_string = lambda_as_string + +def leftshift_as_string(node): + """return an ast.LeftShift node as string""" + return '(%s) << (%s)' % (node.left.as_string(), node.right.as_string()) +LeftShift.as_string = leftshift_as_string + +def list_as_string(node): + """return an ast.List node as string""" + return '[%s]' % ', '.join([child.as_string() for child in node.nodes]) +List.as_string = list_as_string + +def listcomp_as_string(node): + """return an ast.ListComp node as string""" + return '[%s %s]' % (node.expr.as_string(), ' '.join([n.as_string() + for n in node.quals])) +ListComp.as_string = listcomp_as_string + +def listcompfor_as_string(node): + """return an ast.ListCompFor node as string""" + return 'for %s in %s %s' % (node.assign.as_string(), + node.list.as_string(), + ' '.join([n.as_string() for n in node.ifs])) +ListCompFor.as_string = listcompfor_as_string + +def listcompif_as_string(node): + """return an ast.ListCompIf node as string""" + return 'if %s' % node.test.as_string() +ListCompIf.as_string = listcompif_as_string + +def mod_as_string(node): + """return an ast.Mod node as string""" + return '(%s) %% (%s)' % (node.left.as_string(), node.right.as_string()) +Mod.as_string = mod_as_string + +def module_as_string(node): + """return an ast.Module node as string""" + docs = node.doc and '"""%s"""\n' % node.doc or '' + return '%s%s' % (docs, node.node.as_string()) +Module.as_string = module_as_string + +def mul_as_string(node): + """return an ast.Mul node as string""" + return '(%s) * (%s)' % (node.left.as_string(), node.right.as_string()) +Mul.as_string = mul_as_string + +def name_as_string(node): + """return an ast.Name node as string""" + return node.name +Name.as_string = name_as_string + +def not_as_string(node): + """return an ast.Not node as string""" + return 'not %s' % node.expr.as_string() +Not.as_string = not_as_string + +def or_as_string(node): + """return an ast.Or node as string""" + return ' or '.join(['(%s)' % n.as_string() for n in node.nodes]) +Or.as_string = or_as_string + +def pass_as_string(node): + """return an ast.Pass node as string""" + return 'pass' +Pass.as_string = pass_as_string + +def power_as_string(node): + """return an ast.Power node as string""" + return '(%s) ** (%s)' % (node.left.as_string(), node.right.as_string()) +Power.as_string = power_as_string + +def print_as_string(node): + """return an ast.Print node as string""" + nodes = ', '.join([n.as_string() for n in node.nodes]) + if node.dest: + return 'print >> %s, %s,' % (node.dest.as_string(), nodes) + return 'print %s,' % nodes +Print.as_string = print_as_string + +def printnl_as_string(node): + """return an ast.Printnl node as string""" + nodes = ', '.join([n.as_string() for n in node.nodes]) + if node.dest: + return 'print >> %s, %s' % (node.dest.as_string(), nodes) + return 'print %s' % nodes +Printnl.as_string = printnl_as_string + +def raise_as_string(node): + """return an ast.Raise node as string""" + if node.expr1: + if node.expr2: + if node.expr3: + return 'raise %s, %s, %s' % (node.expr1.as_string(), + node.expr2.as_string(), + node.expr3.as_string()) + return 'raise %s, %s' % (node.expr1.as_string(), + node.expr2.as_string()) + return 'raise %s' % node.expr1.as_string() + return 'raise' +Raise.as_string = raise_as_string + +def return_as_string(node): + """return an ast.Return node as string""" + return 'return %s' % node.value.as_string() +Return.as_string = return_as_string + +def rightshift_as_string(node): + """return an ast.RightShift node as string""" + return '(%s) >> (%s)' % (node.left.as_string(), node.right.as_string()) +RightShift.as_string = rightshift_as_string + +def slice_as_string(node): + """return an ast.Slice node as string""" + # FIXME: use flags + lower = node.lower and node.lower.as_string() or '' + upper = node.upper and node.upper.as_string() or '' + return '%s[%s:%s]' % (node.expr.as_string(), lower, upper) +Slice.as_string = slice_as_string + +def sliceobj_as_string(node): + """return an ast.Sliceobj node as string""" + return ':'.join([n.as_string() for n in node.nodes]) +Sliceobj.as_string = sliceobj_as_string + +def stmt_as_string(node): + """return an ast.Stmt node as string""" + stmts = '\n'.join([n.as_string() for n in node.nodes]) + if isinstance(node.parent, Module): + return stmts + return stmts.replace('\n', '\n ') +Stmt.as_string = stmt_as_string + +def sub_as_string(node): + """return an ast.Sub node as string""" + return '(%s) - (%s)' % (node.left.as_string(), node.right.as_string()) +Sub.as_string = sub_as_string + +def subscript_as_string(node): + """return an ast.Subscript node as string""" + # FIXME: flags ? + return '%s[%s]' % (node.expr.as_string(), ','.join([n.as_string() + for n in node.subs])) +Subscript.as_string = subscript_as_string + +def tryexcept_as_string(node): + """return an ast.TryExcept node as string""" + trys = ['try:\n %s' % node.body.as_string()] + for exc_type, exc_obj, body in node.handlers: + if exc_type: + if exc_obj: + excs = 'except %s, %s' % (exc_type.as_string(), + exc_obj.as_string()) + else: + excs = 'except %s' % exc_type.as_string() + else: + excs = 'except' + trys.append('%s:\n %s' % (excs, body.as_string())) + return '\n'.join(trys) +TryExcept.as_string = tryexcept_as_string + +def tryfinally_as_string(node): + """return an ast.TryFinally node as string""" + return 'try:\n %s\nfinally:\n %s' % (node.body.as_string(), + node.final.as_string()) +TryFinally.as_string = tryfinally_as_string + +def tuple_as_string(node): + """return an ast.Tuple node as string""" + return '(%s)' % ', '.join([child.as_string() for child in node.nodes]) +Tuple.as_string = tuple_as_string + +def unaryadd_as_string(node): + """return an ast.UnaryAdd node as string""" + return '+%s' % node.expr.as_string() +UnaryAdd.as_string = unaryadd_as_string + +def unarysub_as_string(node): + """return an ast.UnarySub node as string""" + return '-%s' % node.expr.as_string() +UnarySub.as_string = unarysub_as_string + +def while_as_string(node): + """return an ast.While node as string""" + whiles = 'while %s:\n %s' % (node.test.as_string(), + node.body.as_string()) + if node.else_: + whiles = '%s\nelse:\n %s' % (whiles, node.else_.as_string()) + return whiles +While.as_string = while_as_string + +def yield_as_string(node): + """yield an ast.Yield node as string""" + return 'yield %s' % node.value.as_string() +Yield.as_string = yield_as_string + + +def _import_string(names): + """return a list of (name, asname) formatted as a string + """ + _names = [] + for name, asname in names: + if asname is not None: + _names.append('%s as %s' % (name, asname)) + else: + _names.append(name) + return ', '.join(_names) + +# to backport into compiler ################################################### + +EmptyNode.getChildNodes = lambda self: () diff --git a/raw_building.py b/raw_building.py new file mode 100644 index 00000000..d00fdfb1 --- /dev/null +++ b/raw_building.py @@ -0,0 +1,214 @@ +# 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. +"""this module contains a set of functions to create astng trees from scratch +(build_* functions) or from living object (object_build_* functions) + +:version: $Revision: 1.12 $ +:author: Sylvain Thenault +:copyright: 2003-2005 LOGILAB S.A. (Paris, FRANCE) +:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org +:copyright: 2003-2005 Sylvain Thenault +:contact: mailto:thenault@gmail.com +""" + +__revision__ = "$Id: raw_building.py,v 1.12 2006-03-05 14:44:15 syt Exp $" +__doctype__ = "restructuredtext en" + +import sys +from inspect import getargspec + +from logilab.astng import nodes + +def attach___dict__(node): + """attach the __dict__ attribute to Class and Module objects""" + dictn = nodes.Dict([]) + dictn.parent = node + node.locals['__dict__'] = [dictn] + +def attach_dummy_node(node, name): + """create a dummy node and register it in the locals of the given + node with the specified name + """ + _attach_local_node(node, nodes.EmptyNode(), name) + +def attach_const_node(node, name, value): + """create a Const node and register it in the locals of the given + node with the specified name + """ + _attach_local_node(node, nodes.Const(value), name) + +def attach_import_node(node, modname, membername): + """create a From node and register it in the locals of the given + node with the specified name + """ + _attach_local_node(node, nodes.From(modname, ( (membername, None), ) ), + membername) + +def _attach_local_node(parent, node, name): + node.name = name # needed by add_local_node + node.parent = parent + node.lineno = 1 + parent.add_local_node(node) + + +def build_module(name, doc=None): + """create and initialize a astng Module node""" + node = nodes.Module(doc, nodes.Stmt([])) + node.node.parent = node + node.name = name + node.pure_python = False + node.package = False + node.parent = None + node.globals = node.locals = {} + return node + +def build_class(name, basenames=None, doc=None): + """create and initialize a astng Class node""" + klass = nodes.Class(name, [], doc, nodes.Stmt([])) + bases = [nodes.Name(base) for base in basenames] + for base in bases: + base.parent = klass + klass.basenames = basenames + klass.bases = bases + klass.code.parent = klass + klass.locals = {} + klass.instance_attrs = {} + for name, value in ( ('__name__', name), + #('__module__', node.root().name), + ): + const = nodes.Const(value) + const.parent = klass + klass.locals[name] = [const] + return klass + +# introduction of decorators has changed the Function initializer arguments +if sys.version_info >= (2, 4): + try: + from compiler.ast import Decorators as BaseDecorators + class Decorators(BaseDecorators): + def __init__(self): + BaseDecorators.__init__(self, [], 0) + except ImportError: + Decorators = list + + def build_function(name, args=None, defaults=None, flag=0, doc=None): + """create and initialize a astng Function node""" + args, defaults = args or [], defaults or [] + # first argument is now a list of decorators + func = nodes.Function(Decorators(), name, args, defaults, flag, doc, + nodes.Stmt([])) + func.code.parent = func + func.locals = {} + if args: + register_arguments(func, args) + return func + +else: + def build_function(name, args=None, defaults=None, flag=0, doc=None): + """create and initialize a astng Function node""" + args, defaults = args or [], defaults or [] + func = nodes.Function(name, args, defaults, flag, doc, nodes.Stmt([])) + func.code.parent = func + func.locals = {} + if args: + register_arguments(func, args) + return func + + +def build_name_assign(name, value): + """create and initialize an astng Assign for a name assignment""" + return nodes.Assign([nodes.AssName(name, 'OP_ASSIGN')], nodes.Const(value)) + +def build_attr_assign(name, value, attr='self'): + """create and initialize an astng Assign for an attribute assignment""" + return nodes.Assign([nodes.AssAttr(nodes.Name(attr), name, 'OP_ASSIGN')], + nodes.Const(value)) + +def build_from_import(fromname, names): + """create and intialize an astng From import statement""" + return nodes.From(fromname, [(name, None) for name in names]) + +def register_arguments(node, args): + """add given arguments to local + + args is a list that may contains nested lists + (i.e. def func(a, (b, c, d)): ...) + """ + for arg in args: + if type(arg) is type(''): + node.set_local(arg, node) + else: + register_arguments(node, arg) + + +def object_build_class(node, member): + """create astng for a living class object""" + basenames = [base.__name__ for base in member.__bases__] + return _base_class_object_build(node, member, basenames) + +def object_build_function(node, member): + """create astng for a living function object""" + args, varargs, varkw, defaults = getargspec(member) + if varargs is not None: + args.append(varargs) + if varkw is not None: + args.append(varkw) + func = build_function(member.__name__, args, defaults, + member.func_code.co_flags, member.__doc__) + node.add_local_node(func) + +def object_build_datadescriptor(node, member, name): + """create astng for a living data descriptor object""" + return _base_class_object_build(node, member, [], name) + +def object_build_methoddescriptor(node, member): + """create astng for a living method descriptor object""" + # FIXME get arguments ? + func = build_function(member.__name__, doc=member.__doc__) + # set argnames to None to notice that we have no information, not + # and empty argument list + func.argnames = None + node.add_local_node(func) + +def _base_class_object_build(node, member, basenames, name=None): + """create astng for a living class object, with a given set of base names + (e.g. ancestors) + """ + klass = build_class(name or member.__name__, basenames, member.__doc__) + klass._newstyle = isinstance(member, type) + node.add_local_node(klass) + try: + # limit the instantiation trick since it's too dangerous + # (such as infinite test execution...) + # this at least resolves common case such as Exception.args, + # OSError.errno + if issubclass(member, Exception): + instdict = member().__dict__ + else: + raise TypeError + except: + pass + else: + for name in instdict.keys(): + valnode = nodes.EmptyNode() + valnode.parent = klass + valnode.lineno = 1 + klass.instance_attrs[name] = [valnode] + return klass + + +__all__ = ('register_arguments', 'build_module', + 'object_build_class', 'object_build_function', + 'object_build_datadescriptor', 'object_build_methoddescriptor', + 'attach___dict__', 'attach_dummy_node', + 'attach_const_node', 'attach_import_node') diff --git a/scoped_nodes.py b/scoped_nodes.py new file mode 100644 index 00000000..e86ddd71 --- /dev/null +++ b/scoped_nodes.py @@ -0,0 +1,644 @@ +# 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. +"""This module extends ast "scoped" node, i.e. which are opening a new +local scope in the language definition : Module, Class, Function (and +Lambda in some extends). + +Each new methods and attributes added on each class are documented +below. + + +:version: $Revision: 1.28 $ +:author: Sylvain Thenault +:copyright: 2003-2006 LOGILAB S.A. (Paris, FRANCE) +:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org +:copyright: 2003-2006 Sylvain Thenault +:contact: mailto:thenault@gmail.com +""" + +from __future__ import generators + +__revision__ = "$Id: scoped_nodes.py,v 1.28 2006-04-20 07:37:28 syt Exp $" +__doctype__ = "restructuredtext en" + +import sys +from compiler.ast import Module, Class, Function, Lambda, Dict, Tuple, Raise, \ + Pass, Raise, Yield, Name, Const + +from logilab.common.compat import chain + +from logilab.astng._exceptions import NotFoundError, NoDefault, \ + ASTNGBuildingException, InferenceError +from logilab.astng.utils import extend_class +from logilab.astng import YES, MANAGER, Instance, unpack_infer, _infer_stmts + +# module class dict/iterator interface ######################################## + +class LocalsDictMixIn(object): + """ this class provides locals handling common to Module, Function + and Class nodes, including a dict like interface for direct access + to locals information + + /!\ this class should not be used directly /!\ it's + only used as a methods and attribute container, and update the + original class from the compiler.ast module using its dictionnary + (see below the class definition) + """ + + # attributes below are set by the builder module or by raw factories + + # dictionary of locals with name as key and node defining the local as + # value + locals = None + + + def frame(self): + """return the first node defining a new local scope (i.e. Module, + Function or Class) + """ + return self + + def set_local(self, name, stmt): + """define <name> in locals (<stmt> is the node defining the name) + if the node is a Module node (i.e. has globals), add the name to + globals + + if the name is already defined, ignore it + """ + self.locals.setdefault(name, []).append(stmt) + + __setitem__ = set_local + + def add_local_node(self, child_node): + """append a child which should alter locals to the given node""" + self._append_node(child_node) + self.set_local(child_node.name, child_node) + + def _append_node(self, child_node): + """append a child, linking it in the tree""" + self.code.nodes.append(child_node) + child_node.parent = self + + def __getitem__(self, item): + """method from the `dict` interface returning the first node + associated with the given name in the locals dictionnary + + :type item: str + :param item: the name of the locally defined object + :raises KeyError: if the name is not defined + """ + return self.locals[item][0] + + def __iter__(self): + """method from the `dict` interface returning an iterator on + `self.keys()` + """ + return iter(self.keys()) + + def keys(self): + """method from the `dict` interface returning a tuple containing + locally defined names + """ + return self.locals.keys() +## associated to nodes which are instance of `Function` or +## `Class` +## """ +## # FIXME: sort keys according to line number ? +## try: +## return self.__keys +## except AttributeError: +## keys = [member.name for member in self.locals.values() +## if (isinstance(member, Function) +## or isinstance(member, Class)) +## and member.parent.frame() is self] +## self.__keys = tuple(keys) +## return keys + + def values(self): + """method from the `dict` interface returning a tuple containing + locally defined nodes which are instance of `Function` or `Class` + """ + return [self[key] for key in self.keys()] + + def items(self): + """method from the `dict` interface returning a list of tuple + containing each locally defined name with its associated node, + which is an instance of `Function` or `Class` + """ + return zip(self.keys(), self.values()) + + def has_key(self, name): + """method from the `dict` interface returning True if the given + name is defined in the locals dictionary + """ + return self.locals.has_key(name) + + __contains__ = has_key + +extend_class(Module, LocalsDictMixIn) +extend_class(Class, LocalsDictMixIn) +extend_class(Function, LocalsDictMixIn) +extend_class(Lambda, LocalsDictMixIn) + +class GetattrMixIn(object): + def getattr(self, name, path=None): + try: + return self.locals[name] + except KeyError: + raise NotFoundError(name) + + def igetattr(self, name, path=None): + """infered getattr""" + try: + return _infer_stmts(self.getattr(name, path), name, frame=self, path=path) + except NotFoundError: + raise InferenceError(name) +extend_class(Module, GetattrMixIn) +extend_class(Class, GetattrMixIn) + +# Module ##################################################################### + +class ModuleNG(object): + """/!\ this class should not be used directly /!\ it's + only used as a methods and attribute container, and update the + original class from the compiler.ast module using its dictionnary + (see below the class definition) + """ + + # attributes below are set by the builder module or by raw factories + + # the file from which as been extracted the astng representation. It may + # be None if the representation has been built from a built-in module + file = None + # the module name + name = None + # boolean for astng built from source (i.e. ast) + pure_python = None + # boolean for package module + package = None + # dictionary of globals with name as key and node defining the global + # as value + globals = None + + def getattr(self, name, path=None): + try: + return self.locals[name] + except KeyError: + if self.package: + try: + return [self.import_module(name, relative_only=True)] + except KeyboardInterrupt: + raise + except: + pass + raise NotFoundError(name) + + def _append_node(self, child_node): + """append a child version specific to Module node""" + self.node.nodes.append(child_node) + child_node.parent = self + + def source_line(self): + """return the source line number, 0 on a module""" + return 0 + + def fully_defined(self): + """return True if this module has been built from a .py file + and so contains a complete representation including the code + """ + return self.file is not None and self.file.endswith('.py') + + def statement(self): + """return the first parent node marked as statement node + consider a module as a statement... + """ + return self + + def import_module(self, modname, relative_only=False): + """import the given module considering self as context""" + try: + return MANAGER.astng_from_module_name(self.relative_name(modname)) + except ASTNGBuildingException: + if relative_only: + raise + module = MANAGER.astng_from_module_name(modname) + return module + + def relative_name(self, modname): + if self.package: + return '%s.%s' % (self.name, modname) + package_name = '.'.join(self.name.split('.')[:-1]) + if package_name: + return '%s.%s' % (package_name, modname) + return modname + + def wildcard_import_names(self): + """return the list of imported names when this module is 'wildard + imported' + + It doesn't include the '__builtins__' name which is added by the + current CPython implementation of wildcard imports. + """ + # take advantage of a living module if it exists + try: + living = sys.modules[self.name] + except KeyError: + pass + else: + try: + return living.__all__ + except AttributeError: + return [name for name in living.__dict__.keys() + if not name.startswith('_')] + # else lookup the astng + try: + explicit = self['__all__'].assigned_stmts().next() + # should be a tuple of constant string + return [const.value for const in explicit.nodes] + except (KeyError, AttributeError, InferenceError): + # XXX should admit we have lost if there is something like + # __all__ that we've not been able to analyse (such as + # dynamically constructed __all__) + return [name for name in self.keys() + if not name.startswith('_')] + +extend_class(Module, ModuleNG) + +# Function ################################################################### + +class FunctionNG(object): + """/!\ this class should not be used directly /!\ it's + only used as a methods and attribute container, and update the + original class from the compiler.ast module using its dictionnary + (see below the class definition) + """ + + # attributes below are set by the builder module or by raw factories + + # function's type, 'function' | 'method' | 'staticmethod' | 'classmethod' + type = 'function' + # list of argument names. MAY BE NONE on some builtin functions where + # arguments are unknown + argnames = None + + def is_method(self): + """return true if the function node should be considered as a method""" + return self.type != 'function' + + def is_abstract(self, pass_is_abstract=True): + """return true if the method is abstract + It's considered as abstract if the only statement is a raise of + NotImplementError, or, if pass_is_abstract, a pass statement + """ + for child_node in self.code.getChildNodes(): + if isinstance(child_node, Raise) and child_node.expr1: + try: + name = child_node.expr1.nodes_of_class(Name).next() + if name.name == 'NotImplementedError': + return True + except StopIteration: + pass + if pass_is_abstract and isinstance(child_node, Pass): + return True + return False + # empty function is the same as function with a single "pass" statement + if pass_is_abstract: + return True + + def is_generator(self): + """return true if this is a generator function""" + try: + return self.nodes_of_class(Yield, skip_klass=Function).next() + except StopIteration: + return False + + def format_args(self): + """return arguments formatted as string""" + if self.argnames is None: # information is missing + return '' + result = [] + args, kwargs, last, default_idx = self._pos_information() + for i in range(len(self.argnames)): + name = self.argnames[i] + if type(name) is type(()): + name = '(%s)' % ','.join(name) + if i == last and kwargs: + name = '**%s' % name + elif args and i == last or (kwargs and i == last - 1): + name = '*%s' % name + elif i >= default_idx: + default_str = self.defaults[i - default_idx].as_string() + name = '%s=%s' % (name, default_str) + result.append(name) + return ', '.join(result) + + def default_value(self, argname): + """return the default value for an argument + + :raise `NoDefault`: if there is no default value defined + """ + if self.argnames is None: # information is missing + raise NoDefault() + args, kwargs, last, defaultidx = self._pos_information() + i = self.argnames.index(argname) + if i >= defaultidx and (i - defaultidx) < len(self.defaults): + return self.defaults[i - defaultidx] + raise NoDefault() + + def mularg_class(self, argname): + """if the given argument is a * or ** argument, return respectivly + a Tuple or Dict instance, else return None + """ + args, kwargs, last, defaultidx = self._pos_information() + i = self.argnames.index(argname) + if i == last and kwargs: + valnode = Dict([]) + valnode.parent = self + return valnode + if args and (i == last or (kwargs and i == last - 1)): + valnode = Tuple([]) + valnode.parent = self + return valnode + return None + + def _pos_information(self): + """return a 4-uple with positional information about arguments: + (true if * is used, + true if ** is used, + index of the last argument, + index of the first argument having a default value) + """ + args = self.flags & 4 + kwargs = self.flags & 8 + last = len(self.argnames) - 1 + defaultidx = len(self.argnames) - (len(self.defaults) + + (args and 1 or 0) + + (kwargs and 1 or 0)) + return args, kwargs, last, defaultidx + +extend_class(Function, FunctionNG) + +# lambda nodes may also need some of the function members +Lambda._pos_information = FunctionNG._pos_information.im_func +Lambda.format_args = FunctionNG.format_args.im_func +Lambda.default_value = FunctionNG.default_value.im_func +Lambda.mularg_class = FunctionNG.mularg_class.im_func +Lambda.type = 'function' + +# Class ###################################################################### + +def _class_type(klass): + """return a Class node type to differ metaclass, interface and exception + from 'regular' classes + """ + if klass._type is not None: + return klass._type + if klass.name == 'type': + klass._type = 'metaclass' + elif klass.name.endswith('Interface'): + klass._type = 'interface' + elif klass.name.endswith('Exception'): + klass._type = 'exception' + else: + for base in klass.ancestors(recurs=False): + if base.type != 'class': + klass._type = base.type + break + if klass._type is None: + klass._type = 'class' + return klass._type + +def _class_newstyle(klass): + """return a if the given class is new-style or not + """ + if klass._newstyle is not None: + return klass._newstyle + for base in klass.ancestors(recurs=False): + if base.newstyle: + klass._newstyle = base.newstyle + break + if klass._newstyle is None: + klass._newstyle = False + return klass._newstyle + +def _iface_hdlr(iface_node): + """a handler function used by interfaces to handle suspicious + interface nodes + """ + return True + +class ClassNG(object): + """/!\ this class should not be used directly /!\ it's + only used as a methods and attribute container, and update the + original class from the compiler.ast module using its dictionnary + (see below the class definition) + """ + + _type = None + type = property(_class_type, + doc="class'type, possible values are 'class' | " + "'metaclass' | 'interface' | 'exception'") + + _newstyle = None + newstyle = property(_class_newstyle, + doc="boolean indicating if it's a new style class" + "or not") + + # attributes below are set by the builder module or by raw factories + + # a dictionary of class instances attributes + instance_attrs = None + # list of parent class as a list of string (ie names as they appears + # in the class definition) + basenames = None + + def ancestors(self, recurs=True, path=None): + """return an iterator on the node base classes in a prefixed + depth first order + + :param recurs: + boolean indicating if it should recurse or return direct + ancestors only + """ + # FIXME: should be possible to choose the resolution order + if path is None: + path = [] + for stmt in self.bases: + try: + for baseobj in stmt.infer(path=path): + if not isinstance(baseobj, Class): + # duh ? + continue + yield baseobj + if recurs: + for grandpa in baseobj.ancestors(True, path): + yield grandpa + except InferenceError: + #import traceback + #traceback.print_exc() + # XXX log error ? + continue + + def local_attr_ancestors(self, name, path=None): + """return an iterator on astng representation of parent classes + which have <name> defined in their locals + """ + for astng in self.ancestors(path=path): + if astng.locals.has_key(name): + yield astng + + def instance_attr_ancestors(self, name, path=None): + """return an iterator on astng representation of parent classes + which have <name> defined in their instance attribute dictionary + """ + for astng in self.ancestors(path=path): + if astng.instance_attrs.has_key(name): + yield astng + + def local_attr(self, name, path=None): + """return the astng associated to name in this class locals or + in its parents + + :raises `NotFoundError`: + if no attribute with this name has been find in this class or + its parent classes + """ + try: + return self[name] + except KeyError: + # get if from the first parent implementing it if any + for class_node in self.local_attr_ancestors(name, path): + return class_node[name] + raise NotFoundError(name) + + def instance_attr(self, name, path=None): + """return the astng nodes associated to name in this class instance + attributes dictionary or in its parents + + :raises `NotFoundError`: + if no attribute with this name has been find in this class or + its parent classes + """ + try: + return self.instance_attrs[name] + except KeyError: + # get if from the first parent implementing it if any + for class_node in self.instance_attr_ancestors(name, path): + return class_node.instance_attrs[name] + raise NotFoundError(name) + + def getattr(self, name, path=None): + """this method doesn't look in the instance_attrs dictionary since it's + done by an Instance proxy at inference time. + + It may return a YES object if the attribute has not been actually + found but a __getattr__ or __getattribute__ method is defined + """ + if name in self.locals: + return self.locals[name] + for classnode in self.ancestors(recurs=False, path=path): + try: + return classnode.getattr(name, path) + except NotFoundError: + continue + raise NotFoundError(name) + + def igetattr(self, name, path=None): + """infered getattr, need special treatment in class to handle + descriptors + """ + try: + for infered in _infer_stmts(self.getattr(name, path), name, + frame=self, path=path): + # yield YES object instead of descriptors when necessary + if not isinstance(infered, Const) and isinstance(infered, Instance): + try: + infered._proxied.getattr('__get__', path) + except NotFoundError: + yield infered + else: + yield YES + else: + yield infered + except NotFoundError: + if not name.startswith('__') and self.has_dynamic_getattr(path): + # class handle some dynamic attributes, return a YES object + yield YES + else: + raise InferenceError(name) + + def has_dynamic_getattr(self, path=None): + """return True if the class has a custom __getattr__ or + __getattribute__ method + """ + try: + self.getattr('__getattr__', path) + return True + except NotFoundError: + try: + getattribute = self.getattr('__getattribute__', path)[0] + if getattribute.root().name != '__builtin__': + # class has a custom __getattribute__ defined + return True + except NotFoundError: + pass + return False + + def methods(self): + """return an iterator on all methods defined in the class and + its ancestors + """ + done = {} + for astng in chain(iter((self,)), self.ancestors()): + for meth in astng.mymethods(): + if done.has_key(meth.name): + continue + done[meth.name] = None + yield meth + + def mymethods(self): + """return an iterator on all methods defined in the class""" + for member in self.values(): + if isinstance(member, Function): + yield member + + def interfaces(self, herited=True, handler_func=_iface_hdlr): + """return an iterator on interfaces implemented by the given + class node + """ + # FIXME: what if __implements__ = (MyIFace, MyParent.__implements__)... + try: + implements = Instance(self).getattr('__implements__')[0] + except NotFoundError: + return + if not herited and not implements.frame() is self: + return + oneinf = False + for iface in unpack_infer(implements): + if iface is YES: + continue + if handler_func(iface): + oneinf = True + yield iface + if not oneinf: + raise InferenceError() +## if hasattr(implements, 'nodes'): +## implements = implements.nodes +## else: +## implements = (implements,) +## for iface in implements: +## # let the handler function take care of this.... +## for iface in handler_func(iface): +## yield iface + +extend_class(Class, ClassNG) diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..6eb65124 --- /dev/null +++ b/setup.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python +# pylint: disable-msg=W0142,W0403,W0404,E0611,W0613,W0622,W0622,W0704,R0904 +# +# Copyright (c) 2003 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# 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. +""" Generic Setup script, takes package info from __pkginfo__.py file """ + +from __future__ import nested_scopes + +__revision__ = '$Id: setup.py,v 1.3 2006-01-03 14:30:39 syt Exp $' + +import os +import sys +import shutil +from distutils.core import setup +from distutils.command import install_lib +from os.path import isdir, exists, join, walk + +# import required features +from __pkginfo__ import modname, version, license, short_desc, long_desc, \ + web, author, author_email +# import optional features +try: + from __pkginfo__ import distname +except ImportError: + distname = modname +try: + from __pkginfo__ import scripts +except ImportError: + scripts = [] +try: + from __pkginfo__ import data_files +except ImportError: + data_files = None +try: + from __pkginfo__ import subpackage_of +except ImportError: + subpackage_of = None +try: + from __pkginfo__ import include_dirs +except ImportError: + include_dirs = [] +try: + from __pkginfo__ import ext_modules +except ImportError: + ext_modules = None + +BASE_BLACKLIST = ('CVS', 'debian', 'dist', 'build', '__buildlog') +IGNORED_EXTENSIONS = ('.pyc', '.pyo', '.elc') + + +def ensure_scripts(linux_scripts): + """ + Creates the proper script names required for each platform + (taken from 4Suite) + """ + from distutils import util + if util.get_platform()[:3] == 'win': + scripts_ = [script + '.bat' for script in linux_scripts] + else: + scripts_ = linux_scripts + return scripts_ + + +def get_packages(directory, prefix): + """return a list of subpackages for the given directory + """ + result = [] + for package in os.listdir(directory): + absfile = join(directory, package) + if isdir(absfile): + if exists(join(absfile, '__init__.py')) or \ + package in ('test', 'tests'): + if prefix: + result.append('%s.%s' % (prefix, package)) + else: + result.append(package) + result += get_packages(absfile, result[-1]) + return result + +def export(from_dir, to_dir, + blacklist=BASE_BLACKLIST, + ignore_ext=IGNORED_EXTENSIONS): + """make a mirror of from_dir in to_dir, omitting directories and files + listed in the black list + """ + def make_mirror(arg, directory, fnames): + """walk handler""" + for norecurs in blacklist: + try: + fnames.remove(norecurs) + except ValueError: + pass + for filename in fnames: + # don't include binary files + if filename[-4:] in ignore_ext: + continue + if filename[-1] == '~': + continue + src = '%s/%s' % (directory, filename) + dest = to_dir + src[len(from_dir):] + print >> sys.stderr, src, '->', dest + if os.path.isdir(src): + if not exists(dest): + os.mkdir(dest) + else: + if exists(dest): + os.remove(dest) + shutil.copy2(src, dest) + try: + os.mkdir(to_dir) + except OSError, ex: + # file exists ? + import errno + if ex.errno != errno.EEXIST: + raise + walk(from_dir, make_mirror, None) + + +EMPTY_FILE = '"""generated file, don\'t modify or your data will be lost"""\n' + +class MyInstallLib(install_lib.install_lib): + """extend install_lib command to handle package __init__.py and + include_dirs variable if necessary + """ + def run(self): + """overriden from install_lib class""" + install_lib.install_lib.run(self) + # create Products.__init__.py if needed + if subpackage_of: + product_init = join(self.install_dir, subpackage_of, '__init__.py') + if not exists(product_init): + self.announce('creating %s' % product_init) + stream = open(product_init, 'w') + stream.write(EMPTY_FILE) + stream.close() + # manually install included directories if any + if include_dirs: + if subpackage_of: + base = join(subpackage_of, modname) + else: + base = modname + for directory in include_dirs: + dest = join(self.install_dir, base, directory) + export(directory, dest) + +def install(**kwargs): + """setup entry point""" + if subpackage_of: + package = subpackage_of + '.' + modname + kwargs['package_dir'] = {package : '.'} + packages = [package] + get_packages(os.getcwd(), package) + else: + kwargs['package_dir'] = {modname : '.'} + packages = [modname] + get_packages(os.getcwd(), modname) + kwargs['packages'] = packages + return setup(name = distname, + version = version, + license =license, + description = short_desc, + long_description = long_desc, + author = author, + author_email = author_email, + url = web, + scripts = ensure_scripts(scripts), + data_files=data_files, + ext_modules=ext_modules, + cmdclass={'install_lib': MyInstallLib}, + **kwargs + ) + +if __name__ == '__main__' : + install() diff --git a/test/data/SSL1/Connection1.py b/test/data/SSL1/Connection1.py new file mode 100644 index 00000000..29347e3e --- /dev/null +++ b/test/data/SSL1/Connection1.py @@ -0,0 +1,14 @@ +"""M2Crypto.SSL.Connection
+
+Copyright (c) 1999-2004 Ng Pheng Siong. All rights reserved."""
+
+RCS_id='$Id: Connection1.py,v 1.1 2005-06-13 20:55:22 syt Exp $'
+
+#Some code deleted here
+
+class Connection:
+
+ """An SSL connection."""
+
+ def __init__(self, ctx, sock=None):
+ print 'init Connection'
diff --git a/test/data/SSL1/__init__.py b/test/data/SSL1/__init__.py new file mode 100644 index 00000000..dd588804 --- /dev/null +++ b/test/data/SSL1/__init__.py @@ -0,0 +1 @@ +from Connection1 import Connection
diff --git a/test/data/__init__.py b/test/data/__init__.py new file mode 100644 index 00000000..332e2e72 --- /dev/null +++ b/test/data/__init__.py @@ -0,0 +1 @@ +__revision__="$Id: __init__.py,v 1.1 2005-06-13 20:55:20 syt Exp $" diff --git a/test/data/all.py b/test/data/all.py new file mode 100644 index 00000000..2ba3ae45 --- /dev/null +++ b/test/data/all.py @@ -0,0 +1,10 @@ + + +name = 'a' +_bla = 2 +other = 'o' +class Aaa: pass + +def func(): print 'yo' + +__all__ = 'Aaa', '_bla', 'name' diff --git a/test/data/appl/__init__.py b/test/data/appl/__init__.py new file mode 100644 index 00000000..480109c3 --- /dev/null +++ b/test/data/appl/__init__.py @@ -0,0 +1,4 @@ +#
+"""
+Init
+"""
diff --git a/test/data/appl/myConnection.py b/test/data/appl/myConnection.py new file mode 100644 index 00000000..41fd1565 --- /dev/null +++ b/test/data/appl/myConnection.py @@ -0,0 +1,11 @@ +import SSL1
+class MyConnection(SSL1.Connection):
+
+ """An SSL connection."""
+
+ def __init__(self, dummy):
+ print 'MyConnection init'
+
+if __name__ == '__main__':
+ myConnection = MyConnection(' ')
+ raw_input('Press Enter to continue...')
diff --git a/test/data/module.py b/test/data/module.py new file mode 100644 index 00000000..f6a840bb --- /dev/null +++ b/test/data/module.py @@ -0,0 +1,89 @@ +# -*- coding: Latin-1 -*- +"""test module for astng +""" + +__revision__ = '$Id: module.py,v 1.2 2005-11-02 11:56:54 syt Exp $' + +from logilab.common import modutils, Execute as spawn +from logilab.common.astutils import * +import os.path + +MY_DICT = {} + + +def global_access(key, val): + """function test""" + local = 1 + MY_DICT[key] = val + for i in val: + if i: + del MY_DICT[i] + continue + else: + break + else: + print '!!!' + +class YO: + """hehe""" + a=1 + def __init__(self): + try: + self.yo = 1 + except ValueError, ex: + pass + except (NameError, TypeError): + raise XXXError() + except: + raise + +#print '*****>',YO.__dict__ +class YOUPI(YO): + class_attr = None + + def __init__(self): + self.member = None + + def method(self): + """method test""" + global MY_DICT + try: + MY_DICT = {} + local = None + autre = [a for a, b in MY_DICT if b] + if b in autre: + print 'yo', + elif a in autre: + print 'hehe' + global_access(local, val=autre) + finally: + return local + + def static_method(): + """static method test""" + assert MY_DICT, '???' + static_method = staticmethod(static_method) + + def class_method(cls): + """class method test""" + exec a in b + class_method = classmethod(class_method) + + +def nested_args(a, (b, c, d)): + """nested arguments test""" + print a, b, c, d + while 1: + if a: + break + a += +1 + else: + b += -2 + if c: + d = a and b or c + else: + c = a and b or d + map(lambda x, y: (y, x), a) + +redirect = nested_args + diff --git a/test/data/module2.py b/test/data/module2.py new file mode 100644 index 00000000..cb0b9509 --- /dev/null +++ b/test/data/module2.py @@ -0,0 +1,90 @@ +from __future__ import generators + +from data.module import YO, YOUPI +import data + +class Specialization(YOUPI, YO): pass + +class Metaclass(type): pass + +class Interface: pass + +class MyIFace(Interface): pass + +class AnotherIFace(Interface): pass + +class MyException(Exception): pass +class MyError(MyException): pass + +class AbstractClass(object): + + def to_override(self, whatever): + raise NotImplementedError() + + def return_something(self, param): + if param: + return 'toto' + return + +class Concrete0: + __implements__ = MyIFace +class Concrete1: + __implements__ = MyIFace, AnotherIFace +class Concrete2: + __implements__ = (MyIFace, + AnotherIFace) +class Concrete23(Concrete1): pass + +del YO.member + +del YO +[SYN1, SYN2] = Concrete0, Concrete1 +assert `1` +b = 1 | 2 & 3 ^ 8 +exec 'c = 3' +exec 'c = 3' in {}, {} + +def raise_string(a=2, *args, **kwargs): + raise 'pas glop' + raise Exception, 'yo' + yield 'coucou' + +a = b + 2 +c = b * 2 +c = b / 2 +c = b // 2 +c = b - 2 +c = b % 2 +c = b ** 2 +c = b << 2 +c = b >> 2 +c = ~b + +c = not b + +d = [c] +e = d[:] +e = d[a:b:c] + +raise_string(*args, **kwargs) + +print >> stream, 'bonjour' +print >> stream, 'salut', + + +def make_class(any, base=data.module.YO, *args, **kwargs): + """check base is correctly resolved to Concrete0""" + class Aaaa(base): + """dynamic class""" + return Aaaa + +from os.path import abspath + +import os as myos + + +class A: + pass + +class A(A): + pass diff --git a/test/data/noendingnewline.py b/test/data/noendingnewline.py new file mode 100644 index 00000000..353ded4e --- /dev/null +++ b/test/data/noendingnewline.py @@ -0,0 +1,38 @@ + + +import unittest + + +class TestCase(unittest.TestCase): + + def setUp(self): + unittest.TestCase.setUp(self) + + + def tearDown(self): + unittest.TestCase.tearDown(self) + + def testIt(self): + self.a = 10 + self.xxx() + + + def xxx(self): + if False: + pass + print 'a' + + if False: + pass + pass + + if False: + pass + print 'rara' + + +if __name__ == '__main__': + print 'test2' + unittest.main() + +
\ No newline at end of file diff --git a/test/data/nonregr.py b/test/data/nonregr.py new file mode 100644 index 00000000..a670e39f --- /dev/null +++ b/test/data/nonregr.py @@ -0,0 +1,57 @@ +from __future__ import generators + +try: + enumerate = enumerate +except NameError: + + def enumerate(iterable): + """emulates the python2.3 enumerate() function""" + i = 0 + for val in iterable: + yield i, val + i += 1 + +def toto(value): + for k, v in value: + print v.get('yo') + + +import imp +fp, mpath, desc = imp.find_module('optparse',a) +s_opt = imp.load_module('std_optparse', fp, mpath, desc) + +class OptionParser(s_opt.OptionParser): + + def parse_args(self, args=None, values=None, real_optparse=False): + if real_optparse: + pass +## return super(OptionParser, self).parse_args() + else: + import optcomp + optcomp.completion(self) + + +class Aaa(object): + """docstring""" + def __init__(self): + self.__setattr__('a','b') + pass + + def one_public(self): + """docstring""" + pass + + def another_public(self): + """docstring""" + pass + +class Ccc(Aaa): + """docstring""" + + class Ddd(Aaa): + """docstring""" + pass + + class Eee(Ddd): + """docstring""" + pass diff --git a/test/data/notall.py b/test/data/notall.py new file mode 100644 index 00000000..578495e7 --- /dev/null +++ b/test/data/notall.py @@ -0,0 +1,9 @@ + + +name = 'a' +_bla = 2 +other = 'o' +class Aaa: pass + +def func(): print 'yo' + diff --git a/test/data2/__init__.py b/test/data2/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/test/data2/__init__.py diff --git a/test/data2/clientmodule_test.py b/test/data2/clientmodule_test.py new file mode 100644 index 00000000..71279a80 --- /dev/null +++ b/test/data2/clientmodule_test.py @@ -0,0 +1,32 @@ +""" docstring for file clientmodule.py """ +from data2.suppliermodule_test import Interface as IFace, DoNothing + +class Toto: pass + +class Ancestor: + """ Ancestor method """ + __implements__ = (IFace,) + + def __init__(self, value): + local_variable = 0 + self.attr = 'this method shouldn\'t have a docstring' + self.__value = value + + def get_value(self): + """ nice docstring ;-) """ + return self.__value + + def set_value(self, value): + self.__value = value + return 'this method shouldn\'t have a docstring' + +class Specialization(Ancestor): + TYPE = 'final class' + top = 'class' + + def __init__(self, value, _id): + Ancestor.__init__(self, value) + self._id = _id + self.relation = DoNothing() + self.toto = Toto() + diff --git a/test/data2/suppliermodule_test.py b/test/data2/suppliermodule_test.py new file mode 100644 index 00000000..ddacb477 --- /dev/null +++ b/test/data2/suppliermodule_test.py @@ -0,0 +1,13 @@ +""" file suppliermodule.py """ + +class NotImplemented(Exception): + pass + +class Interface: + def get_value(self): + raise NotImplemented() + + def set_value(self, value): + raise NotImplemented() + +class DoNothing : pass diff --git a/test/fulltest.sh b/test/fulltest.sh new file mode 100755 index 00000000..a6253391 --- /dev/null +++ b/test/fulltest.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +if [ $@ ] ; then + PYVERSIONS=$@ +else + PYVERSIONS="2.2 2.3 2.4" +fi + +for ver in $PYVERSIONS; do + echo "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@" + echo `python$ver -V` + echo "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" + python$ver runtests.py + echo "^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^" + echo `python$ver -V` -OO + python$ver -OO runtests.py +done
\ No newline at end of file diff --git a/test/regrtest.py b/test/regrtest.py new file mode 100644 index 00000000..f2982483 --- /dev/null +++ b/test/regrtest.py @@ -0,0 +1,41 @@ +__revision__ = '$Id: regrtest.py,v 1.8 2006-01-24 19:52:08 syt Exp $' + +import unittest + +from logilab.astng import ResolveError, MANAGER as m +from logilab.astng.builder import ASTNGBuilder, build_module + +import sys +from os.path import abspath +sys.path.insert(1, abspath('regrtest_data')) + +class NonRegressionTC(unittest.TestCase): + +## def test_resolve1(self): +## mod = m.astng_from_module_name('data.nonregr') +## cls = mod['OptionParser'] +## self.assertRaises(ResolveError, cls.resolve_dotted, cls.basenames[0]) +## #self.assert_(cls is not cls.resolve_dotted(cls.basenames[0])) + + def test_module_path(self): + mod = m.astng_from_module_name('import_package_subpackage_module') + package = mod.igetattr('package').next() + self.failUnlessEqual(package.name, 'package') + subpackage = package.igetattr('subpackage').next() + self.failUnlessEqual(subpackage.name, 'package.subpackage') + module = subpackage.igetattr('module').next() + self.failUnlessEqual(module.name, 'package.subpackage.module') + + + def test_living_property(self): + builder = ASTNGBuilder() + builder._done = {} + builder._module = sys.modules[__name__] + builder.object_build(build_module('module_name', ''), Whatever) + + +class Whatever(object): + a = property(lambda x: x, lambda x: x) + +if __name__ == '__main__': + unittest.main() diff --git a/test/regrtest_data/descriptor_crash.py b/test/regrtest_data/descriptor_crash.py new file mode 100644 index 00000000..9cf30124 --- /dev/null +++ b/test/regrtest_data/descriptor_crash.py @@ -0,0 +1,12 @@ +# -*- coding: iso-8859-1 -*- + +import urllib + +class Page(object): + _urlOpen = staticmethod(urllib.urlopen) + + def getPage(self, url): + handle = self._urlOpen(url) + data = handle.read() + handle.close() + return data diff --git a/test/regrtest_data/import_package_subpackage_module.py b/test/regrtest_data/import_package_subpackage_module.py new file mode 100644 index 00000000..8b354c6d --- /dev/null +++ b/test/regrtest_data/import_package_subpackage_module.py @@ -0,0 +1,49 @@ +# pylint: disable-msg=I0011,C0301,W0611 +"""I found some of my scripts trigger off an AttributeError in pylint +0.8.1 (with common 0.12.0 and astng 0.13.1). + +Traceback (most recent call last): + File "/usr/bin/pylint", line 4, in ? + lint.Run(sys.argv[1:]) + File "/usr/lib/python2.4/site-packages/pylint/lint.py", line 729, in __init__ + linter.check(args) + File "/usr/lib/python2.4/site-packages/pylint/lint.py", line 412, in check + self.check_file(filepath, modname, checkers) + File "/usr/lib/python2.4/site-packages/pylint/lint.py", line 426, in check_file + astng = self._check_file(filepath, modname, checkers) + File "/usr/lib/python2.4/site-packages/pylint/lint.py", line 450, in _check_file + self.check_astng_module(astng, checkers) + File "/usr/lib/python2.4/site-packages/pylint/lint.py", line 494, in check_astng_module + self.astng_events(astng, [checker for checker in checkers + File "/usr/lib/python2.4/site-packages/pylint/lint.py", line 511, in astng_events + self.astng_events(child, checkers, _reversed_checkers) + File "/usr/lib/python2.4/site-packages/pylint/lint.py", line 511, in astng_events + self.astng_events(child, checkers, _reversed_checkers) + File "/usr/lib/python2.4/site-packages/pylint/lint.py", line 508, in astng_events + checker.visit(astng) + File "/usr/lib/python2.4/site-packages/logilab/astng/utils.py", line 84, in visit + method(node) + File "/usr/lib/python2.4/site-packages/pylint/checkers/variables.py", line 295, in visit_import + self._check_module_attrs(node, module, name_parts[1:]) + File "/usr/lib/python2.4/site-packages/pylint/checkers/variables.py", line 357, in _check_module_attrs + self.add_message('E0611', args=(name, module.name), +AttributeError: Import instance has no attribute 'name' + + +You can reproduce it by: +(1) create package structure like the following: + +package/ + __init__.py + subpackage/ + __init__.py + module.py + +(2) in package/__init__.py write: + +import subpackage + +(3) run pylint with a script importing package.subpackage.module. +""" +__revision__ = '$Id: import_package_subpackage_module.py,v 1.1 2005-11-10 15:59:32 syt Exp $' +import package.subpackage.module diff --git a/test/regrtest_data/package/__init__.py b/test/regrtest_data/package/__init__.py new file mode 100644 index 00000000..aa5227bd --- /dev/null +++ b/test/regrtest_data/package/__init__.py @@ -0,0 +1,5 @@ +# pylint: disable-msg=R0903 +"""package's __init__ file""" + + +import subpackage diff --git a/test/regrtest_data/package/subpackage/__init__.py b/test/regrtest_data/package/subpackage/__init__.py new file mode 100644 index 00000000..dc4782e6 --- /dev/null +++ b/test/regrtest_data/package/subpackage/__init__.py @@ -0,0 +1 @@ +"""package.subpackage""" diff --git a/test/regrtest_data/package/subpackage/module.py b/test/regrtest_data/package/subpackage/module.py new file mode 100644 index 00000000..4b7244ba --- /dev/null +++ b/test/regrtest_data/package/subpackage/module.py @@ -0,0 +1 @@ +"""package.subpackage.module""" diff --git a/test/runtests.py b/test/runtests.py new file mode 100644 index 00000000..67d66367 --- /dev/null +++ b/test/runtests.py @@ -0,0 +1,5 @@ +from logilab.common.testlib import main + +if __name__ == '__main__': + import sys, os + main(os.path.dirname(sys.argv[0]) or '.') diff --git a/test/unittest_builder.py b/test/unittest_builder.py new file mode 100644 index 00000000..93887e70 --- /dev/null +++ b/test/unittest_builder.py @@ -0,0 +1,314 @@ +# 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. +"""tests for the astng builder module + +Copyright (c) 2003-2005 LOGILAB S.A. (Paris, FRANCE). +http://www.logilab.fr/ -- mailto:contact@logilab.fr +""" + +__revision__ = "$Id: unittest_builder.py,v 1.20 2006-04-19 14:31:41 syt Exp $" + +import unittest +from os.path import join, abspath + +from logilab.common.testlib import TestCase, unittest_main + +from logilab.astng import builder, nodes, Module + +import data +from data import module as test_module + + +class BuilderTC(TestCase): + + def setUp(self): + self.builder = builder.ASTNGBuilder() + + def test_border_cases(self): + """check that a file with no trailing new line is parseable""" + self.builder.file_build('data/noendingnewline.py', 'data.noendingnewline') + self.assertRaises(builder.ASTNGBuildingException, + self.builder.file_build, 'data/inexistant.py', 'whatever') + + def test_inspect_build(self): + """test astng tree build from a living object""" + import __builtin__ + builtin_astng = self.builder.inspect_build(__builtin__) + fclass = builtin_astng['file'] + self.assert_('name' in fclass) + self.assert_('mode' in fclass) + self.assert_('read' in fclass) + self.assert_(fclass.newstyle) + self.assert_(isinstance(fclass['read'], nodes.Function)) + self.assert_(isinstance(fclass['__doc__'], nodes.Const), fclass['__doc__']) + # check builtin function has argnames == None + dclass = builtin_astng['dict'] + self.assertEquals(dclass['has_key'].argnames, None) + # just check type and object are there + builtin_astng.getattr('type') + builtin_astng.getattr('object') + # check property has __init__ + pclass = builtin_astng['property'] + self.assert_('__init__' in pclass) + # + import time + time_astng = self.builder.module_build(time) + self.assert_(time_astng) + # + unittest_astng = self.builder.inspect_build(unittest) + self.failUnless(isinstance(builtin_astng['None'], nodes.Const), builtin_astng['None']) + self.failUnless(isinstance(builtin_astng['Exception'], nodes.From), builtin_astng['Exception']) + self.failUnless(isinstance(builtin_astng['NotImplementedError'], nodes.From)) + + + def test_inspect_build2(self): + """test astng tree build from a living object""" + try: + from mx import DateTime + except ImportError: + self.skip('test skipped: mxDateTime is not available') + else: + dt_astng = self.builder.inspect_build(DateTime) + dt_astng.getattr('DateTime') + # this one is failing since DateTimeType.__module__ = 'builtins' ! + #dt_astng.getattr('DateTimeType') + + def test_inspect_build_instance(self): + """test astng tree build from a living object""" + import exceptions + builtin_astng = self.builder.inspect_build(exceptions) + fclass = builtin_astng['OSError'] + self.assert_('errno' in fclass.instance_attrs) + self.assert_('strerror' in fclass.instance_attrs) + self.assert_('filename' in fclass.instance_attrs) + + def test_package_name(self): + """test base properties and method of a astng module""" + datap = self.builder.file_build('data/__init__.py', 'data') + self.assertEquals(datap.name, 'data') + self.assertEquals(datap.package, 1) + datap = self.builder.file_build('data/__init__.py', 'data.__init__') + self.assertEquals(datap.name, 'data') + self.assertEquals(datap.package, 1) + + def test_object(self): + obj_astng = self.builder.inspect_build(object) + self.failUnless('__setattr__' in obj_astng) + + def test_newstyle_detection(self): + data = ''' +class A: + "old style" + +class B(A): + "old style" + +class C(object): + "new style" + +class D(C): + "new style" + +__metaclass__ = type + +class E(A): + "old style" + +class F: + "new style" +''' + mod_astng = self.builder.string_build(data, __name__, __file__) + self.failIf(mod_astng['A'].newstyle) + self.failIf(mod_astng['B'].newstyle) + self.failUnless(mod_astng['C'].newstyle) + self.failUnless(mod_astng['D'].newstyle) + self.failIf(mod_astng['E'].newstyle) + self.failUnless(mod_astng['F'].newstyle) + + def test_globals(self): + data = ''' +CSTE = 1 + +def update_global(): + global CSTE + CSTE += 1 + +def global_no_effect(): + global CSTE2 + print CSTE +''' + astng = self.builder.string_build(data, __name__, __file__) + self.failUnlessEqual(len(astng.getattr('CSTE')), 2) + self.failUnlessEqual(astng.getattr('CSTE')[0].source_line(), 2) + self.failUnlessEqual(astng.getattr('CSTE')[1].source_line(), 6) + self.assertRaises(nodes.NotFoundError, + astng.getattr, 'CSTE2') + self.assertRaises(nodes.InferenceError, + astng['global_no_effect'].ilookup('CSTE2').next) + + def test_socket_build(self): + import socket + astng = self.builder.module_build(socket) + for fclass in astng.igetattr('socket'): + print fclass.root().name, fclass.name, fclass.lineno + self.assert_('connect' in fclass) + self.assert_('send' in fclass) + self.assert_('close' in fclass) + + +class FileBuildTC(TestCase): + + def setUp(self): + abuilder = builder.ASTNGBuilder() + self.module = abuilder.file_build('data/module.py', 'data.module') + + def test_module_base_props(self): + """test base properties and method of a astng module""" + module = self.module + self.assertEquals(module.name, 'data.module') + self.assertEquals(module.doc, "test module for astng\n") + self.assertEquals(module.source_line(), 0) + self.assertEquals(module.parent, None) + self.assertEquals(module.frame(), module) + self.assertEquals(module.root(), module) + self.assertEquals(module.file, join(abspath(data.__path__[0]), 'module.py')) + self.assertEquals(module.pure_python, 1) + self.assertEquals(module.package, 0) + self.assert_(not module.is_statement()) + self.assertEquals(module.statement(), module) + self.assertEquals(module.node.statement(), module) + + def test_module_locals(self): + """test the 'locals' dictionary of a astng module""" + module = self.module + _locals = module.locals + self.assertEquals(len(_locals), 17) + self.assert_(_locals is module.globals) + keys = _locals.keys() + keys.sort() + self.assertEquals(keys, ['MY_DICT', 'YO', 'YOUPI', '__dict__', + '__doc__', '__file__', '__name__', '__revision__', + 'clean', 'cvrtr', 'debuild', 'global_access', + 'modutils', 'nested_args', 'os', 'redirect', + 'spawn']) + + def test_function_base_props(self): + """test base properties and method of a astng function""" + module = self.module + function = module['global_access'] + self.assertEquals(function.name, 'global_access') + self.assertEquals(function.doc, 'function test') + self.assertEquals(function.source_line(), 14) + self.assert_(function.parent) + self.assertEquals(function.frame(), function) + self.assertEquals(function.parent.frame(), module) + self.assertEquals(function.root(), module) + self.assertEquals(function.argnames, ['key', 'val']) + self.assertEquals(function.type, 'function') + + def test_function_locals(self): + """test the 'locals' dictionary of a astng function""" + _locals = self.module['global_access'].locals + self.assertEquals(len(_locals), 4) + keys = _locals.keys() + keys.sort() + self.assertEquals(keys, ['i', 'key', 'local', 'val']) + + def test_class_base_props(self): + """test base properties and method of a astng class""" + module = self.module + klass = module['YO'] + self.assertEquals(klass.name, 'YO') + self.assertEquals(klass.doc, 'hehe') + self.assertEquals(klass.source_line(), 27) + self.assert_(klass.parent) + self.assertEquals(klass.frame(), klass) + self.assertEquals(klass.parent.frame(), module) + self.assertEquals(klass.root(), module) + self.assertEquals(klass.basenames, []) + self.assertEquals(klass.newstyle, False) + self.failUnless(isinstance(klass['__doc__'], nodes.Const)) + + def test_class_locals(self): + """test the 'locals' dictionary of a astng class""" + module = self.module + klass1 = module['YO'] + klass2 = module['YOUPI'] + locals1 = klass1.locals + locals2 = klass2.locals + keys = locals1.keys() + keys.sort() + self.assertEquals(keys, ['__dict__', '__doc__', '__init__', '__module__', '__name__', + 'a']) + keys = locals2.keys() + keys.sort() + self.assertEquals(keys, ['__dict__', '__doc__', '__init__', '__module__', '__name__', + 'class_attr', + 'class_method', 'method', 'static_method']) + + def test_class_instance_attrs(self): + module = self.module + klass1 = module['YO'] + klass2 = module['YOUPI'] + self.assertEquals(klass1.instance_attrs.keys(), ['yo']) + self.assertEquals(klass2.instance_attrs.keys(), ['member']) + + def test_class_basenames(self): + module = self.module + klass1 = module['YO'] + klass2 = module['YOUPI'] + self.assertEquals(klass1.basenames, []) + self.assertEquals(klass2.basenames, ['YO']) + + def test_method_base_props(self): + """test base properties and method of a astng method""" + klass2 = self.module['YOUPI'] + # "normal" method + method = klass2['method'] + self.assertEquals(method.name, 'method') + self.assertEquals(method.argnames, ['self']) + self.assertEquals(method.doc, 'method test') + self.assertEquals(method.source_line(), 47) + self.assertEquals(method.type, 'method') + # class method + method = klass2['class_method'] + self.assertEquals(method.argnames, ['cls']) + self.assertEquals(method.type, 'classmethod') + # static method + method = klass2['static_method'] + self.assertEquals(method.argnames, []) + self.assertEquals(method.type, 'staticmethod') + + def test_method_locals(self): + """test the 'locals' dictionary of a astng method""" + klass2 = self.module['YOUPI'] + method = klass2['method'] + _locals = method.locals + self.assertEquals(len(_locals), 5) + keys = _locals.keys() + keys.sort() + self.assertEquals(keys, ['a', 'autre', 'b', 'local', 'self']) + + +class ModuleBuildTC(FileBuildTC): + + def setUp(self): + abuilder = builder.ASTNGBuilder() + self.module = abuilder.module_build(test_module) + + +__all__ = ('BuilderModuleBuildTC', 'BuilderFileBuildTC', 'BuilderTC') + +if __name__ == '__main__': + # unittest.main() + unittest_main() diff --git a/test/unittest_inference.py b/test/unittest_inference.py new file mode 100644 index 00000000..39ef21cf --- /dev/null +++ b/test/unittest_inference.py @@ -0,0 +1,557 @@ +# 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. +"""tests for the astng inference capabilities + +Copyright (c) 2005-2006 LOGILAB S.A. (Paris, FRANCE). +http://www.logilab.fr/ -- mailto:contact@logilab.fr +""" + +__revision__ = "$Id: unittest_inference.py,v 1.20 2006-04-19 15:17:41 syt Exp $" + +from os.path import join, abspath + +from logilab.astng import builder, nodes, inference, utils, YES, Instance +from logilab.common.testlib import TestCase, unittest_main + +def get_name_node(start_from, name, index=0): + return [n for n in start_from.nodes_of_class(nodes.Name) if n.name == name][index] + +def get_node_of_class(start_from, klass): + return start_from.nodes_of_class(klass).next() + +builder = builder.ASTNGBuilder() + +class InferenceUtilsTC(TestCase): + + def test_path_wrapper(self): + infer_default = inference.path_wrapper(inference.infer_default) + infer_end = inference.path_wrapper(inference.infer_end) + self.failUnlessRaises(inference.InferenceError, + infer_default(1).next) + self.failUnlessEqual(infer_end(1).next(), 1) + +class InferenceTC(TestCase): + + DATA = ''' +import exceptions + +class C(object): + "new style" + attr = 4 + + def meth1(self, arg1, optarg=0): + var = object() + print "yo", arg1, optarg + self.iattr = "hop" + return var + + def meth2(self): + self.meth1(*self.meth3) + + def meth3(self, d=attr): + b = self.attr + c = self.iattr + return b, c + +ex = exceptions.Exception("msg") +v = C().meth1(1) +a, b, c = ex, 1, "bonjour" +[d, e, f] = [ex, 1.0, ("bonjour", v)] +g, h = f +i, (j, k) = u"glup", f + +a, b= b, a # Gasp ! +''' + + def setUp(self): + self.astng = builder.string_build(self.DATA, __name__, __file__) + + def test_module_inference(self): + infered = self.astng.infer() + obj = infered.next() + self.failUnlessEqual(obj.name, __name__) + self.failUnlessEqual(obj.root().name, __name__) + self.failUnlessRaises(StopIteration, infered.next) + + def test_class_inference(self): + infered = self.astng['C'].infer() + obj = infered.next() + self.failUnlessEqual(obj.name, 'C') + self.failUnlessEqual(obj.root().name, __name__) + self.failUnlessRaises(StopIteration, infered.next) + + def test_function_inference(self): + infered = self.astng['C']['meth1'].infer() + obj = infered.next() + self.failUnlessEqual(obj.name, 'meth1') + self.failUnlessEqual(obj.root().name, __name__) + self.failUnlessRaises(StopIteration, infered.next) + + def test_name_inference(self): + infered = self.astng['C']['meth1']['var'].infer() + var = infered.next() + self.failUnlessEqual(var.name, 'object') + self.failUnlessEqual(var.root().name, '__builtin__') + self.failUnlessRaises(StopIteration, infered.next) + + def test_tupleassign_name_inference(self): + infered = self.astng['a'].infer() + exc = infered.next() + self.failUnless(isinstance(exc, inference.Instance)) + self.failUnlessEqual(exc.name, 'Exception') + self.failUnlessEqual(exc.root().name, 'exceptions') + self.failUnlessRaises(StopIteration, infered.next) + infered = self.astng['b'].infer() + const = infered.next() + self.failUnless(isinstance(const, nodes.Const)) + self.failUnlessEqual(const.value, 1) + self.failUnlessRaises(StopIteration, infered.next) + infered = self.astng['c'].infer() + const = infered.next() + self.failUnless(isinstance(const, nodes.Const)) + self.failUnlessEqual(const.value, "bonjour") + self.failUnlessRaises(StopIteration, infered.next) + + def test_listassign_name_inference(self): + infered = self.astng['d'].infer() + exc = infered.next() + self.failUnless(isinstance(exc, inference.Instance)) + self.failUnlessEqual(exc.name, 'Exception') + self.failUnlessEqual(exc.root().name, 'exceptions') + self.failUnlessRaises(StopIteration, infered.next) + infered = self.astng['e'].infer() + const = infered.next() + self.failUnless(isinstance(const, nodes.Const)) + self.failUnlessEqual(const.value, 1.0) + self.failUnlessRaises(StopIteration, infered.next) + infered = self.astng['f'].infer() + const = infered.next() + self.failUnless(isinstance(const, nodes.Tuple)) + self.failUnlessRaises(StopIteration, infered.next) + + def test_advanced_tupleassign_name_inference1(self): + infered = self.astng['g'].infer() + const = infered.next() + self.failUnless(isinstance(const, nodes.Const)) + self.failUnlessEqual(const.value, "bonjour") + self.failUnlessRaises(StopIteration, infered.next) + infered = self.astng['h'].infer() + var = infered.next() + self.failUnlessEqual(var.name, 'object') + self.failUnlessEqual(var.root().name, '__builtin__') + self.failUnlessRaises(StopIteration, infered.next) + + def test_advanced_tupleassign_name_inference2(self): + infered = self.astng['i'].infer() + const = infered.next() + self.failUnless(isinstance(const, nodes.Const)) + self.failUnlessEqual(const.value, u"glup") + self.failUnlessRaises(StopIteration, infered.next) + infered = self.astng['j'].infer() + const = infered.next() + self.failUnless(isinstance(const, nodes.Const)) + self.failUnlessEqual(const.value, "bonjour") + self.failUnlessRaises(StopIteration, infered.next) + infered = self.astng['k'].infer() + var = infered.next() + self.failUnlessEqual(var.name, 'object') + self.failUnlessEqual(var.root().name, '__builtin__') + self.failUnlessRaises(StopIteration, infered.next) + + def test_swap_assign_inference(self): + infered = self.astng.locals['a'][1].infer() + const = infered.next() + self.failUnless(isinstance(const, nodes.Const)) + self.failUnlessEqual(const.value, 1) + self.failUnlessRaises(StopIteration, infered.next) + infered = self.astng.locals['b'][1].infer() + exc = infered.next() + self.failUnless(isinstance(exc, inference.Instance)) + self.failUnlessEqual(exc.name, 'Exception') + self.failUnlessEqual(exc.root().name, 'exceptions') + self.failUnlessRaises(StopIteration, infered.next) + + def test_getattr_inference1(self): + infered = self.astng['ex'].infer(path=[]) + exc = infered.next() + self.failUnless(isinstance(exc, inference.Instance)) + self.failUnlessEqual(exc.name, 'Exception') + self.failUnlessEqual(exc.root().name, 'exceptions') + self.failUnlessRaises(StopIteration, infered.next) + + def test_getattr_inference2(self): + infered = get_node_of_class(self.astng['C']['meth2'], nodes.Getattr).infer() + meth1 = infered.next() + self.failUnlessEqual(meth1.name, 'meth1') + self.failUnlessEqual(meth1.root().name, __name__) + self.failUnlessRaises(StopIteration, infered.next) + + def test_getattr_inference3(self): + infered = self.astng['C']['meth3']['b'].infer() + const = infered.next() + self.failUnless(isinstance(const, nodes.Const)) + self.failUnlessEqual(const.value, 4) + self.failUnlessRaises(StopIteration, infered.next) + + def test_getattr_inference4(self): + infered = self.astng['C']['meth3']['c'].infer() + const = infered.next() + self.failUnless(isinstance(const, nodes.Const)) + self.failUnlessEqual(const.value, "hop") + self.failUnlessRaises(StopIteration, infered.next) + + def test_callfunc_inference(self): + infered = self.astng['v'].infer() + meth1 = infered.next() + self.failUnless(isinstance(meth1, inference.Instance)) + self.failUnlessEqual(meth1.name, 'object') + self.failUnlessEqual(meth1.root().name, '__builtin__') + self.failUnlessRaises(StopIteration, infered.next) + + def test_args_default_inference1(self): + optarg = get_name_node(self.astng['C']['meth1'], 'optarg') + infered = optarg.infer() + obj1 = infered.next() + self.failUnless(isinstance(obj1, nodes.Const)) + self.failUnlessEqual(obj1.value, 0) + obj1 = infered.next() + self.failUnless(obj1 is YES) + self.failUnlessRaises(StopIteration, infered.next) + + def test_args_default_inference2(self): + infered = self.astng['C']['meth3']['d'].infer(name='d') + obj1 = infered.next() + self.failUnless(isinstance(obj1, nodes.Const)) + self.failUnlessEqual(obj1.value, 4) + obj1 = infered.next() + self.failUnless(obj1 is YES) + self.failUnlessRaises(StopIteration, infered.next) + + def test_inference_restrictions(self): + infered = get_name_node(self.astng['C']['meth1'], 'arg1').infer() + obj1 = infered.next() + self.failUnless(obj1 is YES) + self.failUnlessRaises(StopIteration, infered.next) + + def test_del(self): + data = ''' +del undefined_attr + ''' + astng = builder.string_build(data, __name__, __file__) + self.failUnlessRaises(inference.InferenceError, + astng.node.getChildNodes()[0].infer().next) + + def test_ancestors_inference(self): + data = ''' +class A: + pass + +class A(A): + pass + ''' + astng = builder.string_build(data, __name__, __file__) + a1 = astng.locals['A'][0] + a2 = astng.locals['A'][1] + a2_ancestors = list(a2.ancestors()) + self.failUnlessEqual(len(a2_ancestors), 1) + self.failUnless(a2_ancestors[0] is a1) + + def test_ancestors_inference2(self): + data = ''' +class A: + pass + +class B(A): pass + +class A(B): + pass + ''' + astng = builder.string_build(data, __name__, __file__) + a1 = astng.locals['A'][0] + a2 = astng.locals['A'][1] + a2_ancestors = list(a2.ancestors()) + self.failUnlessEqual(len(a2_ancestors), 2) + self.failUnless(a2_ancestors[0] is astng.locals['B'][0]) + self.failUnless(a2_ancestors[1] is a1, a2_ancestors[1]) + + + def test_f_arg_f(self): + data = ''' +def f(f=1): + return f + +a = f() + ''' + astng = builder.string_build(data, __name__, __file__) + a = astng['a'] + a_infer = a.infer() + self.failUnlessEqual(a_infer.next().value, 1) + self.failUnlessEqual(a_infer.next(), YES) + self.failUnlessRaises(StopIteration, a_infer.next) + + def test_exc_ancestors(self): + data = ''' +def f(): + raise NotImplementedError + ''' + astng = builder.string_build(data, __name__, __file__) + names = astng.nodes_of_class(nodes.Name) + nie = names.next().infer().next() + self.failUnless(isinstance(nie, nodes.Class)) + nie_ancestors = [c.name for c in nie.ancestors()] + self.failUnlessEqual(nie_ancestors, ['RuntimeError', 'StandardError', 'Exception']) + + def test_except_inference(self): + data = ''' +try: + print hop +except NameError, ex: + ex1 = ex +except Exception, ex: + ex2 = ex + raise + ''' + astng = builder.string_build(data, __name__, __file__) + ex1 = astng['ex1'] + ex1_infer = ex1.infer() + infered = list(ex1.infer()) + #print 'EX1:', ex1 + #print 'INFEREND', infered + ex1 = ex1_infer.next() + self.failUnless(isinstance(ex1, inference.Instance)) + self.failUnlessEqual(ex1.name, 'NameError') + self.failUnlessRaises(StopIteration, ex1_infer.next) + ex2 = astng['ex2'] + ex2_infer = ex2.infer() + ex2 = ex2_infer.next() + self.failUnless(isinstance(ex2, inference.Instance)) + self.failUnlessEqual(ex2.name, 'Exception') + self.failUnlessRaises(StopIteration, ex2_infer.next) + + def test_del(self): + data = ''' +a = 1 +b = a +del a +c = a +a = 2 +d = a + ''' + astng = builder.string_build(data, __name__, __file__) + n = astng['b'] + n_infer = n.infer() + infered = n_infer.next() + self.failUnless(isinstance(infered, nodes.Const)) + self.failUnlessEqual(infered.value, 1) + self.failUnlessRaises(StopIteration, n_infer.next) + n = astng['c'] + n_infer = n.infer() + self.failUnlessRaises(inference.InferenceError, n_infer.next) + n = astng['d'] + n_infer = n.infer() + infered = n_infer.next() + self.failUnless(isinstance(infered, nodes.Const)) + self.failUnlessEqual(infered.value, 2) + self.failUnlessRaises(StopIteration, n_infer.next) + + def test_builtin_types(self): + data = ''' +l = [1] +t = (2,) +d = {} +s = '' +u = u'' + ''' + astng = builder.string_build(data, __name__, __file__) + n = astng['l'] + infered = n.infer().next() + self.failUnless(isinstance(infered, nodes.List)) + self.failUnless(isinstance(infered, inference.Instance)) + self.failUnlessEqual(infered.getitem(0).value, 1) + self.failUnless(isinstance(infered._proxied, nodes.Class)) + self.failUnlessEqual(infered._proxied.name, 'list') + self.failUnless('append' in infered._proxied.locals) + n = astng['t'] + infered = n.infer().next() + self.failUnless(isinstance(infered, nodes.Tuple)) + self.failUnless(isinstance(infered, inference.Instance)) + self.failUnlessEqual(infered.getitem(0).value, 2) + self.failUnless(isinstance(infered._proxied, nodes.Class)) + self.failUnlessEqual(infered._proxied.name, 'tuple') + n = astng['d'] + infered = n.infer().next() + self.failUnless(isinstance(infered, nodes.Dict)) + self.failUnless(isinstance(infered, inference.Instance)) + self.failUnless(isinstance(infered._proxied, nodes.Class)) + self.failUnlessEqual(infered._proxied.name, 'dict') + self.failUnless('get' in infered._proxied.locals) + n = astng['s'] + infered = n.infer().next() + self.failUnless(isinstance(infered, nodes.Const)) + self.failUnless(isinstance(infered, inference.Instance)) + self.failUnlessEqual(infered.name, 'str') + self.failUnless('lower' in infered._proxied.locals) + n = astng['u'] + infered = n.infer().next() + self.failUnless(isinstance(infered, nodes.Const)) + self.failUnless(isinstance(infered, inference.Instance)) + self.failUnlessEqual(infered.name, 'unicode') + self.failUnless('lower' in infered._proxied.locals) + + def test_descriptor_are_callable(self): + data = ''' +class A: + statm = staticmethod(open) + clsm = classmethod('whatever') + ''' + astng = builder.string_build(data, __name__, __file__) + statm = astng['A'].igetattr('statm').next() + self.failUnless(statm.callable()) + clsm = astng['A'].igetattr('clsm').next() + self.failUnless(clsm.callable()) + + def test_bt_ancestor_crash(self): + data = ''' +class Warning(Warning): + pass + ''' + astng = builder.string_build(data, __name__, __file__) + w = astng['Warning'] + ancestors = w.ancestors() + ancestor = ancestors.next() + self.failUnlessEqual(ancestor.name, 'Warning') + self.failUnlessEqual(ancestor.root().name, 'exceptions') + ancestor = ancestors.next() + self.failUnlessEqual(ancestor.name, 'Exception') + self.failUnlessEqual(ancestor.root().name, 'exceptions') + self.failUnlessRaises(StopIteration, ancestors.next) + + def test_qqch(self): + data = ''' +from logilab.common.modutils import load_module_from_name +xxx = load_module_from_name('__pkginfo__') + ''' + astng = builder.string_build(data, __name__, __file__) + xxx = astng['xxx'] + infered = list(xxx.infer()) + self.failUnlessEqual([n.__class__ for n in infered], + [nodes.Const, YES.__class__, YES.__class__]) + + def test_method_argument(self): + data = ''' +class ErudiEntitySchema: + """a entity has a type, a set of subject and or object relations""" + def __init__(self, e_type, **kwargs): + kwargs['e_type'] = e_type.capitalize().encode() + + def meth(self, e_type, *args, **kwargs): + kwargs['e_type'] = e_type.capitalize().encode() + print args + ''' + astng = builder.string_build(data, __name__, __file__) + arg = get_name_node(astng['ErudiEntitySchema']['__init__'], 'e_type') + self.failUnlessEqual([n.__class__ for n in arg.infer()], + [YES.__class__]) + arg = get_name_node(astng['ErudiEntitySchema']['__init__'], 'kwargs') + self.failUnlessEqual([n.__class__ for n in arg.infer()], + [nodes.Dict]) + arg = get_name_node(astng['ErudiEntitySchema']['meth'], 'e_type') + self.failUnlessEqual([n.__class__ for n in arg.infer()], + [YES.__class__]) + arg = get_name_node(astng['ErudiEntitySchema']['meth'], 'args') + self.failUnlessEqual([n.__class__ for n in arg.infer()], + [nodes.Tuple]) + arg = get_name_node(astng['ErudiEntitySchema']['meth'], 'kwargs') + self.failUnlessEqual([n.__class__ for n in arg.infer()], + [nodes.Dict]) + + + def test_tuple_then_list(self): + data = ''' +def test_view(rql, vid, tags=()): + tags = list(tags) + tags.append(vid) + ''' + astng = builder.string_build(data, __name__, __file__) + name = get_name_node(astng['test_view'], 'tags', -1) + it = name.infer() + tags = it.next() + self.failUnlessEqual(tags.__class__, Instance) + self.failUnlessEqual(tags._proxied.name, 'list') + self.failUnlessRaises(StopIteration, it.next) + + + + def test_mulassign_inference(self): + data = ''' + +def first_word(line): + """Return the first word of a line""" + + return line.split()[0] + +def last_word(line): + """Return last word of a line""" + + return line.split()[-1] + +def process_line(word_pos): + """Silly function: returns (ok, callable) based on argument. + + For test purpose only. + """ + + if word_pos > 0: + return (True, first_word) + elif word_pos < 0: + return (True, last_word) + else: + return (False, None) + +if __name__ == '__main__': + + line_number = 0 + for a_line in file('test_callable.py'): + tupletest = process_line(line_number) + (ok, fct) = process_line(line_number) + if ok: + fct(a_line) +''' + astng = builder.string_build(data, __name__, __file__) + self.failUnlessEqual(len(list(astng['process_line'].infer_call_result(None))), + 3) + self.failUnlessEqual(len(list(astng['tupletest'].infer())), + 3) + self.failUnlessEqual([str(infered) + for infered in astng['fct'].infer()], + ['Function(first_word)', 'Function(last_word)', 'Const(None)']) + + def test_float_complex_ambiguity(self): + data = ''' +def no_conjugate_member(magic_flag): + """should not raise E1101 on something.conjugate""" + if magic_flag: + something = 1.0 + else: + something = 1.0j + if isinstance(something, float): + return something + return something.conjugate() + ''' + astng = builder.string_build(data, __name__, __file__) + self.failUnlessEqual([i.value for i in astng['no_conjugate_member'].ilookup('something')], + [1.0, 1.0j]) + +if __name__ == '__main__': + from logilab.common.testlib import unittest_main + unittest_main() diff --git a/test/unittest_inspector.py b/test/unittest_inspector.py new file mode 100644 index 00000000..7556a082 --- /dev/null +++ b/test/unittest_inspector.py @@ -0,0 +1,88 @@ +# Copyright (c) 2000-2002 LOGILAB S.A. (Paris, FRANCE). +# http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# 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. +""" +unittest for the visitors.diadefs module +""" + +__revision__ = "$Id: unittest_inspector.py,v 1.4 2006-01-26 00:12:59 syt Exp $" + +import unittest +import sys + +from logilab import astng +from logilab.astng import ASTNGManager, nodes, inspector + +def astng_wrapper(func, modname): + return func(modname) + + +class LinkerTC(unittest.TestCase): + + def setUp(self): + self.project = ASTNGManager().project_from_files(['data2'], astng_wrapper) + self.linker = inspector.Linker(self.project) + self.linker.visit(self.project) + + def test_class_implements(self): + klass = self.project.get_module('data2.clientmodule_test')['Ancestor'] + self.assert_(hasattr(klass, 'implements')) + self.assertEqual(len(klass.implements), 1) + self.assert_(isinstance(klass.implements[0], nodes.Class)) + self.assertEqual(klass.implements[0].name, "Interface") + klass = self.project.get_module('data2.clientmodule_test')['Specialization'] + self.assert_(hasattr(klass, 'implements')) + self.assertEqual(len(klass.implements), 0) + + def test_locals_assignment_resolution(self): + klass = self.project.get_module('data2.clientmodule_test')['Specialization'] + self.assert_(hasattr(klass, 'locals_type')) + type_dict = klass.locals_type + self.assertEqual(len(type_dict), 2) + keys = type_dict.keys() + keys.sort() + self.assertEqual(keys, ['TYPE', 'top']) + self.assertEqual(len(type_dict['TYPE']), 1) + self.assertEqual(type_dict['TYPE'][0].value, 'final class') + self.assertEqual(len(type_dict['top']), 1) + self.assertEqual(type_dict['top'][0].value, 'class') + + def test_instance_attrs_resolution(self): + klass = self.project.get_module('data2.clientmodule_test')['Specialization'] + self.assert_(hasattr(klass, 'instance_attrs_type')) + type_dict = klass.instance_attrs_type + self.assertEqual(len(type_dict), 3) + keys = type_dict.keys() + keys.sort() + self.assertEqual(keys, ['_id', 'relation', 'toto']) + self.assert_(isinstance(type_dict['relation'][0], astng.Instance), type_dict['relation']) + self.assertEqual(type_dict['relation'][0].name, 'DoNothing') + self.assert_(isinstance(type_dict['toto'][0], astng.Instance), type_dict['toto']) + self.assertEqual(type_dict['toto'][0].name, 'Toto') + self.assert_(type_dict['_id'][0] is astng.YES, type_dict['_id']) + + +class LinkerTC2(LinkerTC): + + def setUp(self): + self.project = ASTNGManager().from_directory('data2') + self.linker = inspector.Linker(self.project) + self.linker.visit(self.project) + +__all__ = ('LinkerTC', 'LinkerTC2') + + +if __name__ == '__main__': + unittest.main() diff --git a/test/unittest_lookup.py b/test/unittest_lookup.py new file mode 100644 index 00000000..62cbd22f --- /dev/null +++ b/test/unittest_lookup.py @@ -0,0 +1,122 @@ +# 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. +"""tests for the astng variable lookup capabilities + +Copyright (c) 2005-2006 LOGILAB S.A. (Paris, FRANCE). +http://www.logilab.fr/ -- mailto:contact@logilab.fr +""" + +__revision__ = "$Id: unittest_lookup.py,v 1.2 2006-03-03 09:29:50 syt Exp $" + +from os.path import join, abspath + +from logilab.astng import builder, nodes, scoped_nodes, \ + InferenceError, NotFoundError +#from logilab.astng import builder, nodes, inference, utils, YES +from logilab.common.testlib import TestCase, unittest_main + +builder = builder.ASTNGBuilder() +MODULE = builder.file_build('data/module.py', 'data.module') +MODULE2 = builder.file_build('data/module2.py', 'data.module2') +NONREGR = builder.file_build('data/nonregr.py', 'data.nonregr') + +class LookupTC(TestCase): + + def test_limit(self): + data = ''' +l = [a + for a,b in list] + +a = 1 +b = a +a = None + +def func(): + c = 1 + ''' + astng = builder.string_build(data, __name__, __file__) + names = astng.nodes_of_class(nodes.Name) + a = names.next() + stmts = a.lookup('a')[1] + self.failUnlessEqual(len(stmts), 1) + b = astng.locals['b'][1] + #self.failUnlessEqual(len(b.lookup('b')[1]), 1) + self.failUnlessEqual(len(astng.lookup('b')[1]), 2) + b_infer = b.infer() + b_value = b_infer.next() + self.failUnlessEqual(b_value.value, 1) + self.failUnlessRaises(StopIteration, b_infer.next) + func = astng.locals['func'][0] + self.failUnlessEqual(len(func.lookup('c')[1]), 1) + + def test_module(self): + astng = builder.string_build('pass', __name__, __file__) + # built-in objects + none = astng.ilookup('None').next() + self.assertEquals(none.value, None) + obj = astng.ilookup('object').next() + self.assert_(isinstance(obj, nodes.Class)) + self.assertEquals(obj.name, 'object') + self.assertRaises(InferenceError, astng.ilookup('YOAA').next) + + # XXX + self.assertEquals(len(list(NONREGR.ilookup('enumerate'))), 2) + + def test_class_ancestor_name(self): + data = ''' +class A: + pass + +class A(A): + pass + ''' + astng = builder.string_build(data, __name__, __file__) + cls1 = astng.locals['A'][0] + cls2 = astng.locals['A'][1] + name = cls2.nodes_of_class(nodes.Name).next() + self.assertEquals(name.infer().next(), cls1) + + ### backport those test to inline code + def test_method(self): + method = MODULE['YOUPI']['method'] + my_dict = method.ilookup('MY_DICT').next() + self.assert_(isinstance(my_dict, nodes.Dict), my_dict) + none = method.ilookup('None').next() + self.assertEquals(none.value, None) + self.assertRaises(InferenceError, method.ilookup('YOAA').next) + + def test_function_argument_with_default(self): + make_class = MODULE2['make_class'] + base = make_class.ilookup('base').next() + self.assert_(isinstance(base, nodes.Class), base.__class__) + self.assertEquals(base.name, 'YO') + self.assertEquals(base.root().name, 'data.module') + + def test_class(self): + klass = MODULE['YOUPI'] + #print klass.getattr('MY_DICT') + my_dict = klass.ilookup('MY_DICT').next() + self.assert_(isinstance(my_dict, nodes.Dict)) + none = klass.ilookup('None').next() + self.assertEquals(none.value, None) + obj = klass.ilookup('object').next() + self.assert_(isinstance(obj, nodes.Class)) + self.assertEquals(obj.name, 'object') + self.assertRaises(InferenceError, klass.ilookup('YOAA').next) + + def test_inner_classes(self): + ccc = NONREGR['Ccc'] + self.assertEquals(ccc.ilookup('Ddd').next().name, 'Ddd') + +if __name__ == '__main__': + unittest_main() diff --git a/test/unittest_manager.py b/test/unittest_manager.py new file mode 100644 index 00000000..fd01036f --- /dev/null +++ b/test/unittest_manager.py @@ -0,0 +1,70 @@ +import unittest +import os +from os.path import join +from logilab.astng.manager import ASTNGManager + + +class ASTNGManagerTC(unittest.TestCase): + def setUp(self): + self.manager = ASTNGManager() + + def test_astng_from_module(self): + astng = self.manager.astng_from_module(unittest) + self.assertEquals(astng.pure_python, True) + import time + astng = self.manager.astng_from_module(time) + self.assertEquals(astng.pure_python, False) + + def test_astng_from_class(self): + astng = self.manager.astng_from_class(file) + self.assertEquals(astng.name, 'file') + self.assertEquals(astng.parent.frame().name, '__builtin__') + + astng = self.manager.astng_from_class(object) + self.assertEquals(astng.name, 'object') + self.assertEquals(astng.parent.frame().name, '__builtin__') + self.failUnless('__setattr__' in astng) + + + + def test_from_directory(self): + obj = self.manager.from_directory('data') + self.assertEquals(obj.name, 'data') + self.assertEquals(obj.path, join(os.getcwd(), 'data')) + + def test_package_node(self): + obj = self.manager.from_directory('data') + expected_short = ['SSL1', '__init__', 'all', 'appl', 'module', 'module2', + 'noendingnewline', 'nonregr', 'notall'] + expected_long = ['SSL1', 'data', 'data.all', 'appl', 'data.module', + 'data.module2', 'data.noendingnewline', 'data.nonregr', + 'data.notall'] + self.assertEquals(obj.keys(), expected_short) + self.assertEquals([m.name for m in obj.values()], expected_long) + self.assertEquals([m for m in list(obj)], expected_short) + self.assertEquals([(name, m.name) for name, m in obj.items()], + zip(expected_short, expected_long)) + self.assertEquals([(name, m.name) for name, m in obj.items()], + zip(expected_short, expected_long)) + + self.assertEquals('module' in obj, True) + self.assertEquals(obj.has_key('module'), True) + self.assertEquals(obj.get('module').name, 'data.module') + self.assertEquals(obj['module'].name, 'data.module') + self.assertEquals(obj.get('whatever'), None) + + #self.assertEquals(obj.has_key, []) + #self.assertEquals(obj.get, []) + #for key in obj: + # print key, + # print obj[key] + + self.assertEquals(obj.fullname(), 'data') + # FIXME: test fullname on a subpackage + +__all__ = ('ASTNGManagerTC',) + +if __name__ == '__main__': + unittest.main() + + diff --git a/test/unittest_nodes.py b/test/unittest_nodes.py new file mode 100644 index 00000000..5d65cf0a --- /dev/null +++ b/test/unittest_nodes.py @@ -0,0 +1,77 @@ +# 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. +"""tests for specific behaviour of astng nodes + +Copyright (c) 2003-2005 LOGILAB S.A. (Paris, FRANCE). +http://www.logilab.fr/ -- mailto:contact@logilab.fr +""" + +__revision__ = "$Id: unittest_nodes.py,v 1.6 2005-12-28 14:56:21 syt Exp $" + +import unittest + +from logilab.astng import builder, nodes, NotFoundError + +from data import module as test_module + +abuilder = builder.ASTNGBuilder() +MODULE = abuilder.module_build(test_module) +MODULE2 = abuilder.file_build('data/module2.py', 'data.module2') + + +class ImportNodeTC(unittest.TestCase): + + def test_import_self_resolve(self): + import_ = MODULE2['myos'] + myos = import_.infer('myos').next() + self.failUnless(isinstance(myos, nodes.Module), myos) + self.failUnlessEqual(myos.name, 'os') + + def test_from_self_resolve(self): + from_ = MODULE['modutils'] + spawn = from_.infer('spawn').next() + self.failUnless(isinstance(spawn, nodes.Class), spawn) + self.failUnlessEqual(spawn.root().name, 'logilab.common') + from_ = MODULE2['abspath'] + abspath = from_.infer('abspath').next() + self.failUnless(isinstance(abspath, nodes.Function), abspath) + self.failUnlessEqual(abspath.root().name, 'os.path') + + def test_real_name(self): + from_ = MODULE['modutils'] + self.assertEquals(from_.real_name('spawn'), 'Execute') + imp_ = MODULE['os'] + self.assertEquals(imp_.real_name('os'), 'os') + self.assertRaises(NotFoundError, imp_.real_name, 'os.path') + imp_ = MODULE['spawn'] + self.assertEquals(imp_.real_name('spawn'), 'Execute') + self.assertRaises(NotFoundError, imp_.real_name, 'Execute') + imp_ = MODULE2['YO'] + self.assertEquals(imp_.real_name('YO'), 'YO') + self.assertRaises(NotFoundError, imp_.real_name, 'data') + + def test_as_string(self): + ast = MODULE['modutils'] + self.assertEquals(ast.as_string(), "from logilab.common import modutils, Execute as spawn") + ast = MODULE['os'] + self.assertEquals(ast.as_string(), "import os.path") + +class CmpNodeTC(unittest.TestCase): + def test_as_string(self): + ast = abuilder.string_build("a == 2") + self.assertEquals(ast.as_string(), "a == 2") + +__all__ = ('ImportNodeTC',) + +if __name__ == '__main__': + unittest.main() diff --git a/test/unittest_scoped_nodes.py b/test/unittest_scoped_nodes.py new file mode 100644 index 00000000..41bc03e0 --- /dev/null +++ b/test/unittest_scoped_nodes.py @@ -0,0 +1,272 @@ +# 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. +"""tests for specific behaviour of astng scoped nodes (ie module, class and +function) + +Copyright (c) 2003-2006 LOGILAB S.A. (Paris, FRANCE). +http://www.logilab.fr/ -- mailto:contact@logilab.fr +""" + +__revision__ = "$Id: unittest_scoped_nodes.py,v 1.12 2006-03-05 14:44:15 syt Exp $" + +import unittest +import sys + +from logilab.common.compat import sorted + +from logilab.astng import builder, nodes, scoped_nodes, \ + InferenceError, NotFoundError + +abuilder = builder.ASTNGBuilder() +MODULE = abuilder.file_build('data/module.py', 'data.module') +MODULE2 = abuilder.file_build('data/module2.py', 'data.module2') +NONREGR = abuilder.file_build('data/nonregr.py', 'data.nonregr') + +def _test_dict_interface(self, node, test_attr): + self.assert_(node[test_attr] is node[test_attr]) + self.assert_(test_attr in node) + node.keys() + node.values() + node.items() + iter(node) + + +class ModuleNodeTC(unittest.TestCase): + + def test_dict_interface(self): + _test_dict_interface(self, MODULE, 'YO') + + def test_getattr(self): + yo = MODULE.getattr('YO')[0] + self.assert_(isinstance(yo, nodes.Class)) + self.assertEquals(yo.name, 'YO') + red = MODULE.igetattr('redirect').next() + self.assert_(isinstance(red, nodes.Function)) + self.assertEquals(red.name, 'nested_args') + spawn = MODULE.igetattr('spawn').next() + self.assert_(isinstance(spawn, nodes.Class)) + self.assertEquals(spawn.name, 'Execute') + # resolve packageredirection + sys.path.insert(1, 'data') + try: + m = abuilder.file_build('data/appl/myConnection.py', 'appl.myConnection') + cnx = m.igetattr('SSL1').next().igetattr('Connection').next() + self.assertEquals(cnx.__class__, nodes.Class) + self.assertEquals(cnx.name, 'Connection') + self.assertEquals(cnx.root().name, 'SSL1.Connection1') + finally: + del sys.path[1] + self.assertEquals(len(NONREGR.getattr('enumerate')), 2) + # raise ResolveError + self.assertRaises(InferenceError, MODULE.igetattr, 'YOAA') + + def test_wildard_import_names(self): + m = abuilder.file_build('data/all.py', 'all') + self.assertEquals(m.wildcard_import_names(), ['Aaa', '_bla', 'name']) + m = abuilder.file_build('data/notall.py', 'notall') + res = m.wildcard_import_names() + res.sort() + self.assertEquals(res, ['Aaa', 'func', 'name', 'other']) + + def test_as_string(self): + """just check as_string on a whole module doesn't raise an exception + """ + self.assert_(MODULE.as_string()) + self.assert_(MODULE2.as_string()) + + +class FunctionNodeTC(unittest.TestCase): + + def test_dict_interface(self): + _test_dict_interface(self, MODULE['global_access'], 'local') + + def test_default_value(self): + func = MODULE2['make_class'] + self.assert_(isinstance(func.default_value('base'), nodes.Getattr)) + self.assertRaises(scoped_nodes.NoDefault, func.default_value, 'args') + self.assertRaises(scoped_nodes.NoDefault, func.default_value, 'kwargs') + self.assertRaises(scoped_nodes.NoDefault, func.default_value, 'any') + self.assert_(isinstance(func.mularg_class('args'), nodes.Tuple)) + self.assert_(isinstance(func.mularg_class('kwargs'), nodes.Dict)) + self.assertEquals(func.mularg_class('base'), None) + + def test_navigation(self): + function = MODULE['global_access'] + self.assertEquals(function.statement(), function) + l_sibling = function.previous_sibling() + self.assert_(isinstance(l_sibling, nodes.Assign)) + self.assert_(l_sibling is function.getChildNodes()[0].previous_sibling()) + r_sibling = function.next_sibling() + self.assert_(isinstance(r_sibling, nodes.Class)) + self.assertEquals(r_sibling.name, 'YO') + self.assert_(r_sibling is function.getChildNodes()[0].next_sibling()) + last = r_sibling.next_sibling().next_sibling().next_sibling() + self.assert_(isinstance(last, nodes.Assign)) + self.assertEquals(last.next_sibling(), None) + first = l_sibling.previous_sibling().previous_sibling().previous_sibling().previous_sibling() + self.assertEquals(first.previous_sibling(), None) + + def test_nested_args(self): + func = MODULE['nested_args'] + self.assertEquals(func.argnames, ['a', ('b', 'c', 'd')]) + local = func.keys() + local.sort() + self.assertEquals(local, ['a', 'b', 'c', 'd']) + self.assertEquals(func.type, 'function') + + def test_format_args(self): + func = MODULE2['make_class'] + self.assertEquals(func.format_args(), 'any, base=data.module.YO, *args, **kwargs') + func = MODULE['nested_args'] + self.assertEquals(func.format_args(), 'a, (b,c,d)') + + def test_is_abstract(self): + method = MODULE2['AbstractClass']['to_override'] + self.assert_(method.is_abstract(pass_is_abstract=False)) + method = MODULE2['AbstractClass']['return_something'] + self.assert_(not method.is_abstract(pass_is_abstract=False)) + # non regression : test raise "string" doesn't cause an exception in is_abstract + func = MODULE2['raise_string'] + self.assert_(not func.is_abstract(pass_is_abstract=False)) + +## def test_raises(self): +## method = MODULE2['AbstractClass']['to_override'] +## self.assertEquals([str(term) for term in method.raises()], +## ["CallFunc(Name('NotImplementedError'), [], None, None)"] ) + +## def test_returns(self): +## method = MODULE2['AbstractClass']['return_something'] +## # use string comp since Node doesn't handle __cmp__ +## self.assertEquals([str(term) for term in method.returns()], +## ["Const('toto')", "Const(None)"]) + + +class ClassNodeTC(unittest.TestCase): + + def test_dict_interface(self): + _test_dict_interface(self, MODULE['YOUPI'], 'method') + + def test_navigation(self): + klass = MODULE['YO'] + self.assertEquals(klass.statement(), klass) + l_sibling = klass.previous_sibling() + self.assert_(isinstance(l_sibling, nodes.Function), l_sibling) + self.assertEquals(l_sibling.name, 'global_access') + r_sibling = klass.next_sibling() + self.assert_(isinstance(r_sibling, nodes.Class)) + self.assertEquals(r_sibling.name, 'YOUPI') + + def test_local_attr_ancestors(self): + klass2 = MODULE['YOUPI'] + it = klass2.local_attr_ancestors('__init__') + anc_klass = it.next() + self.assert_(isinstance(anc_klass, nodes.Class)) + self.assertEquals(anc_klass.name, 'YO') + self.assertRaises(StopIteration, it.next) + it = klass2.local_attr_ancestors('method') + self.assertRaises(StopIteration, it.next) + + def test_instance_attr_ancestors(self): + klass2 = MODULE['YOUPI'] + it = klass2.instance_attr_ancestors('yo') + anc_klass = it.next() + self.assert_(isinstance(anc_klass, nodes.Class)) + self.assertEquals(anc_klass.name, 'YO') + self.assertRaises(StopIteration, it.next) + klass2 = MODULE['YOUPI'] + it = klass2.instance_attr_ancestors('member') + self.assertRaises(StopIteration, it.next) + + def test_methods(self): + klass2 = MODULE['YOUPI'] + methods = [m.name for m in klass2.methods()] + methods.sort() + self.assertEquals(methods, ['__init__', 'class_method', + 'method', 'static_method']) + methods = [m.name for m in klass2.mymethods()] + methods.sort() + self.assertEquals(methods, ['__init__', 'class_method', + 'method', 'static_method']) + klass2 = MODULE2['Specialization'] + methods = [m.name for m in klass2.mymethods()] + methods.sort() + self.assertEquals(methods, []) + self.assertEquals(klass2.local_attr('method').name, 'method') + self.assertRaises(NotFoundError, klass2.local_attr, 'nonexistant') + methods = [m.name for m in klass2.methods()] + methods.sort() + self.assertEquals(methods, ['__init__', 'class_method', + 'method', 'static_method']) + + #def test_rhs(self): + # my_dict = MODULE['MY_DICT'] + # self.assert_(isinstance(my_dict.rhs(), nodes.Dict)) + # a = MODULE['YO']['a'] + # value = a.rhs() + # self.assert_(isinstance(value, nodes.Const)) + # self.assertEquals(value.value, 1) + + def test_ancestors(self): + klass = MODULE['YOUPI'] + ancs = [a.name for a in klass.ancestors()] + self.assertEquals(ancs, ['YO']) + klass = MODULE2['Specialization'] + ancs = [a.name for a in klass.ancestors()] + self.assertEquals(ancs, ['YOUPI', 'YO', 'YO']) + + def test_type(self): + klass = MODULE['YOUPI'] + self.assertEquals(klass.type, 'class') + klass = MODULE2['Metaclass'] + self.assertEquals(klass.type, 'metaclass') + klass = MODULE2['MyException'] + self.assertEquals(klass.type, 'exception') + klass = MODULE2['MyIFace'] + self.assertEquals(klass.type, 'interface') + klass = MODULE2['MyError'] + self.assertEquals(klass.type, 'exception') + + def test_interfaces(self): + for klass, interfaces in (('Concrete0', ['MyIFace']), + ('Concrete1', ['MyIFace', 'AnotherIFace']), + ('Concrete2', ['MyIFace', 'AnotherIFace']), + ('Concrete23', ['MyIFace', 'AnotherIFace'])): + klass = MODULE2[klass] + self.assertEquals([i.name for i in klass.interfaces()], + interfaces) + + def test_inner_classes(self): + eee = NONREGR['Ccc']['Eee'] + self.assertEquals([n.name for n in eee.ancestors()], ['Ddd', 'Aaa', 'object']) + + + def test_classmethod_attributes(self): + data = ''' +class WebAppObject(object): + def registered(cls, application): + cls.appli = application + cls.schema = application.schema + cls.config = application.config + return cls + registered = classmethod(registered) + ''' + astng = abuilder.string_build(data, __name__, __file__) + cls = astng['WebAppObject'] + self.assertEquals(sorted(cls.locals.keys()), + ['__dict__', '__doc__', '__module__', '__name__', + 'appli', 'config', 'registered', 'schema']) + +__all__ = ('ModuleNodeTC', 'ImportNodeTC', 'FunctionNodeTC', 'ClassNodeTC') + +if __name__ == '__main__': + unittest.main() diff --git a/utils.py b/utils.py new file mode 100644 index 00000000..24158c1d --- /dev/null +++ b/utils.py @@ -0,0 +1,175 @@ +# 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. +"""this module contains some utilities to navigate in the tree or to +extract information from it + +:version: $Revision: 1.18 $ +:author: Sylvain Thenault +:copyright: 2003-2005 LOGILAB S.A. (Paris, FRANCE) +:contact: http://www.logilab.fr/ -- mailto:python-projects@logilab.org +:copyright: 2003-2005 Sylvain Thenault +:contact: mailto:thenault@gmail.com +""" + +__revision__ = "$Id: utils.py,v 1.18 2006-01-03 15:51:41 syt Exp $" +__doctype__ = "restructuredtext en" + +from logilab.common.compat import enumerate +from logilab.astng._exceptions import IgnoreChild + +def extend_class(original, addons): + """add methods and attribute defined in the addons class to the original + class + """ + brain = addons.__dict__.copy() + for special_key in ('__doc__', '__module__'): + if special_key in addons.__dict__: + del brain[special_key] + original.__dict__.update(brain) + +class ASTWalker: + """a walker visiting a tree in preorder, calling on the handler: + + * visit_<class name> on entering a node, where class name is the class of + the node in lower case + + * leave_<class name> on leaving a node, where class name is the class of + the node in lower case + """ + def __init__(self, handler): + self.handler = handler + self._cache = {} + + def walk(self, node): + """walk on the tree from <node>, getting callbacks from handler + """ + try: + self.visit(node) + except IgnoreChild: + pass + else: + for child_node in node.getChildNodes(): + self.walk(child_node) + self.leave(node) + + def get_callbacks(self, node): + """get callbacks from handler for the visited node + """ + klass = node.__class__ + methods = self._cache.get(klass) + if methods is None: + handler = self.handler + kid = klass.__name__.lower() + e_method = getattr(handler, 'visit_%s' % kid, + getattr(handler, 'visit_default', None)) + l_method = getattr(handler, 'leave_%s' % kid, + getattr(handler, 'leave_default', None)) + self._cache[klass] = (e_method, l_method) + else: + e_method, l_method = methods + return e_method, l_method + + def visit(self, node): + """walk on the tree from <node>, getting callbacks from handler""" + method = self.get_callbacks(node)[0] + if method is not None: + method(node) + + def leave(self, node): + """walk on the tree from <node>, getting callbacks from handler""" + method = self.get_callbacks(node)[1] + if method is not None: + method(node) + + +class LocalsVisitor(ASTWalker): + """visit a project by traversing the locals dictionnary""" + def __init__(self): + ASTWalker.__init__(self, self) + self._visited = {} + + def visit(self, node): + """launch the visit starting from the given node""" + if self._visited.has_key(node): + return + self._visited[node] = 1 + methods = self.get_callbacks(node) + recurse = 1 + if methods[0] is not None: + try: + methods[0](node) + except IgnoreChild: + recurse = 0 + if recurse: + if hasattr(node, 'locals'): + for local_node in node.values(): + self.visit(local_node) + if methods[1] is not None: + return methods[1](node) + +def are_exclusive(stmt1, stmt2): + """return true if the two given statement are mutually exclusive + + algorithm : + 1) index stmt1's parents + 2) climb among stmt2's parents until we find a common parent + 3) if the common parent is a If or TryExcept statement, look if nodes are + in exclusive branchs + """ + from logilab.astng.nodes import If, TryExcept + # index stmt1's parents + stmt1_parents = {} + children = {} + node = stmt1.parent + previous = stmt1 + while node: + stmt1_parents[node] = 1 + children[node] = previous + previous = node + node = node.parent + # climb among stmt2's parents until we find a common parent + node = stmt2.parent + previous = stmt2 + while node: + if stmt1_parents.has_key(node): + # if the common parent is a If or TryExcept statement, look if + # nodes are in exclusive branchs + if isinstance(node, If): + if previous != children[node]: + return True + elif isinstance(node, TryExcept): + stmt1_previous = children[node] + if not previous is stmt1_previous: + stmt1_branch, stmt1_num = _try_except_from_branch(node, stmt1_previous) + stmt2_branch, stmt2_num = _try_except_from_branch(node, previous) + if stmt1_branch != stmt1_branch: + if not ((stmt2_branch == 'body' and stmt1_branch == 'else') or + (stmt1_branch == 'body' and stmt2_branch == 'else') or + (stmt2_branch == 'body' and stmt1_branch == 'except') or + (stmt1_branch == 'body' and stmt2_branch == 'except')): + return True + elif stmt1_num != stmt2_num: + return True + return False + previous = node + node = node.parent + return False + +def _try_except_from_branch(node, stmt): + if stmt is node.body: + return 'body', 1 + if stmt is node.else_: + return 'else', 1 + for i, block_nodes in enumerate(node.handlers): + if stmt in block_nodes: + return 'except', i |