diff options
41 files changed, 1775 insertions, 990 deletions
@@ -2,6 +2,9 @@ Change log for the astroid package (used to be astng) ===================================================== -- + * The logilab-common dependency is not needed anymore as the needed code + was integrated into astroid. + * Generated enum member stubs now support IntEnum and multiple base classes. @@ -234,7 +237,46 @@ Change log for the astroid package (used to be astng) in a non-package will finally result in an import-error on Pylint's side. Until now relative_only was ignored, leading to the import of `something`, if it was globally available. - + + * Add get_wrapping_class API to scoped_nodes, which can be used to + retrieve the class that wraps a node. + + * Class.getattr looks by default in the implicit and the explicit metaclasses, + which is `type` on Python 3. + + Closes issue #114. + + * There's a new separate step for transforms. + + Until now, the transforms were applied at the same time the tree was + being built. This was problematic if the transform functions were + using inference, since the inference was executed on a partially + constructed tree, which led to failures when post-building + information was needed (such as setting the _from_names + for the From imports). + Now there's a separate step for transforms, which are applied + using transform.TransformVisitor. + There's a couple of other related changes: + + * astroid.parse and AstroidBuilder gained a new parameter + `apply_transforms`, which is a boolean flag, which will + control if the transforms are applied. We do this because + there are uses when the vanilla tree is wanted, without + any implicit modification. + + * the transforms are also applied for builtin modules, + as a side effect of the fact that transform visiting + was moved in AstroidBuilder._post_build from + AstroidBuilder._data_build. + + Closes issue #116. + + * Class._explicit_metaclass is now a public API, in the form of + Class.declared_metaclass. + + Class.mro remains the de facto method for retrieving the metaclass + of a class, which will also do an evaluation of what declared_metaclass + returns. 2015-03-14 -- 1.3.6 @@ -59,7 +59,6 @@ Test ---- Tests are in the 'test' subdirectory. To launch the whole tests suite -at once, you may use the 'pytest' utility from logilab-common (simply -type 'pytest' from within this directory) or using unittest discover:: +at once, you can use unittest discover:: python -m unittest discover -p "unittest*.py" diff --git a/astroid/__init__.py b/astroid/__init__.py index 3a8fdc01..25f00bc4 100644 --- a/astroid/__init__.py +++ b/astroid/__init__.py @@ -60,10 +60,11 @@ from astroid import inference # more stuff available from astroid import raw_building -from astroid.bases import YES, Instance, BoundMethod, UnboundMethod +from astroid.bases import Instance, BoundMethod, UnboundMethod from astroid.node_classes import are_exclusive, unpack_infer from astroid.scoped_nodes import builtin_lookup from astroid.builder import parse +from astroid.util import YES # make a manager instance (borg) accessible from astroid package from astroid.manager import AstroidManager diff --git a/astroid/__pkginfo__.py b/astroid/__pkginfo__.py index 9c184e6d..f70df7ce 100644 --- a/astroid/__pkginfo__.py +++ b/astroid/__pkginfo__.py @@ -23,7 +23,7 @@ modname = 'astroid' numversion = (1, 3, 6) version = '.'.join([str(num) for num in numversion]) -install_requires = ['logilab-common>=0.63.0', 'six'] +install_requires = ['six'] license = 'LGPL' diff --git a/astroid/as_string.py b/astroid/as_string.py index ecada1e1..cc0051cf 100644 --- a/astroid/as_string.py +++ b/astroid/as_string.py @@ -136,8 +136,14 @@ class AsStringVisitor(object): """return an astroid.CallFunc node as string""" expr_str = node.func.accept(self) args = [arg.accept(self) for arg in node.args] + if node.keywords: + keywords = [kwarg.accept(self) for kwarg in node.keywords] + else: + keywords = [] + if node.starargs: args.append('*' + node.starargs.accept(self)) + args.extend(keywords) if node.kwargs: args.append('**' + node.kwargs.accept(self)) return '%s(%s)' % (expr_str, ', '.join(args)) diff --git a/astroid/bases.py b/astroid/bases.py index 9b4c2fe1..98c8149e 100644 --- a/astroid/bases.py +++ b/astroid/bases.py @@ -18,16 +18,14 @@ """This module contains base classes and functions for the nodes and some inference utils. """ -__docformat__ = "restructuredtext en" import sys import warnings -from contextlib import contextmanager -from logilab.common.decorators import cachedproperty - -from astroid.exceptions import (InferenceError, AstroidError, NotFoundError, - UnresolvableName, UseInferenceDefault) +from astroid import context as contextmod +from astroid import decorators as decoratorsmod +from astroid import exceptions +from astroid import util if sys.version_info >= (3, 0): @@ -56,7 +54,7 @@ def _is_property(meth): if PROPERTIES.intersection(meth.decoratornames()): return True stripped = {name.split(".")[-1] for name in meth.decoratornames() - if name is not YES} + if name is not util.YES} return any(name in stripped for name in POSSIBLE_PROPERTIES) @@ -80,51 +78,6 @@ class Proxy(object): yield self -class InferenceContext(object): - __slots__ = ('path', 'lookupname', 'callcontext', 'boundnode', 'inferred') - - def __init__(self, path=None, inferred=None): - self.path = path or set() - self.lookupname = None - self.callcontext = None - self.boundnode = None - self.inferred = inferred or {} - - def push(self, node): - name = self.lookupname - if (node, name) in self.path: - raise StopIteration() - self.path.add((node, name)) - - def clone(self): - # XXX copy lookupname/callcontext ? - clone = InferenceContext(self.path, inferred=self.inferred) - clone.callcontext = self.callcontext - clone.boundnode = self.boundnode - return clone - - def cache_generator(self, key, generator): - results = [] - for result in generator: - results.append(result) - yield result - - self.inferred[key] = tuple(results) - return - - @contextmanager - def restore_path(self): - path = set(self.path) - yield - self.path = path - -def copy_context(context): - if context is not None: - return context.clone() - else: - return InferenceContext() - - def _infer_stmts(stmts, context, frame=None): """Return an iterator on statements inferred by each statement in *stmts*.""" stmt = None @@ -134,10 +87,10 @@ def _infer_stmts(stmts, context, frame=None): context = context.clone() else: name = None - context = InferenceContext() + context = contextmod.InferenceContext() for stmt in stmts: - if stmt is YES: + if stmt is util.YES: yield stmt inferred = True continue @@ -146,32 +99,13 @@ def _infer_stmts(stmts, context, frame=None): for inferred in stmt.infer(context=context): yield inferred inferred = True - except UnresolvableName: + except excpetions.UnresolvableName: continue - except InferenceError: - yield YES + except exceptions.InferenceError: + yield util.YES inferred = True if not inferred: - raise InferenceError(str(stmt)) - - -class _Yes(object): - """Special inference object, which is returned when inference fails.""" - def __repr__(self): - return 'YES' - - def __getattribute__(self, name): - if name == 'next': - raise AttributeError('next method should not be called') - if name.startswith('__') and name.endswith('__'): - return super(_Yes, self).__getattribute__(name) - return self - - def __call__(self, *args, **kwargs): - return self - - -YES = _Yes() + raise exceptions.InferenceError(str(stmt)) def _infer_method_result_truth(instance, method_name, context): @@ -180,12 +114,12 @@ def _infer_method_result_truth(instance, method_name, context): meth = next(instance.igetattr(method_name, context=context), None) if meth and hasattr(meth, 'infer_call_result'): for value in meth.infer_call_result(instance, context=context): - if value is YES: + if value is util.YES: return value inferred = next(value.infer(context=context)) return inferred.bool_value() - return YES + return util.YES class Instance(Proxy): @@ -194,7 +128,7 @@ class Instance(Proxy): def getattr(self, name, context=None, lookupclass=True): try: values = self._proxied.instance_attr(name, context) - except NotFoundError: + except exceptions.NotFoundError: if name == '__class__': return [self._proxied] if lookupclass: @@ -202,21 +136,23 @@ class Instance(Proxy): # unless they are explicitly defined. if name in ('__name__', '__bases__', '__mro__', '__subclasses__'): return self._proxied.local_attr(name) - return self._proxied.getattr(name, context) - raise NotFoundError(name) + return self._proxied.getattr(name, context, + class_context=False) + raise exceptions.NotFoundError(name) # since we've no context information, return matching class members as # well if lookupclass: try: - return values + self._proxied.getattr(name, context) - except NotFoundError: + return values + self._proxied.getattr(name, context, + class_context=False) + except exceptions.NotFoundError: pass return values def igetattr(self, name, context=None): """inferred getattr""" if not context: - context = InferenceContext() + context = contextmod.InferenceContext() try: # avoid recursively inferring the same attr on the same class context.push((self._proxied, name)) @@ -227,14 +163,14 @@ class Instance(Proxy): context, frame=self, ) - except NotFoundError: + except exceptions.NotFoundError: try: # fallback to class'igetattr since it has some logic to handle # descriptors return self._wrap_attr(self._proxied.igetattr(name, context), context) - except NotFoundError: - raise InferenceError(name) + except exceptions.NotFoundError: + raise exceptions.InferenceError(name) def _wrap_attr(self, attrs, context=None): """wrap bound methods of attrs in a InstanceMethod proxies""" @@ -263,13 +199,13 @@ class Instance(Proxy): """infer what a class instance is returning when called""" inferred = False for node in self._proxied.igetattr('__call__', context): - if node is YES: + if node is util.YES: continue for res in node.infer_call_result(caller, context): inferred = True yield res if not inferred: - raise InferenceError() + raise exceptions.InferenceError() def __repr__(self): return '<Instance of %s.%s at 0x%s>' % (self._proxied.root().name, @@ -281,9 +217,9 @@ class Instance(Proxy): def callable(self): try: - self._proxied.getattr('__call__') + self._proxied.getattr('__call__', class_context=False) return True - except NotFoundError: + except exceptions.NotFoundError: return False def pytype(self): @@ -305,14 +241,14 @@ class Instance(Proxy): nonzero. If a class defines neither __len__() nor __bool__(), all its instances are considered true. """ - context = InferenceContext() + context = contextmod.InferenceContext() try: result = _infer_method_result_truth(self, BOOL_SPECIAL_METHOD, context) - except (InferenceError, NotFoundError): + except (exceptions.InferenceError, exceptions.NotFoundError): # Fallback to __len__. try: result = _infer_method_result_truth(self, '__len__', context) - except (NotFoundError, InferenceError): + except (exceptions.NotFoundError, exceptions.InferenceError): return True return result @@ -351,7 +287,7 @@ class UnboundMethod(Proxy): if (self._proxied.name == '__new__' and self._proxied.parent.frame().qname() == '%s.object' % BUILTINS): infer = caller.args[0].infer() if caller.args else [] - return ((x is YES and x or Instance(x)) for x in infer) + return ((x is util.YES and x or Instance(x)) for x in infer) return self._proxied.infer_call_result(caller, context) def bool_value(self): @@ -370,7 +306,7 @@ class BoundMethod(UnboundMethod): def infer_call_result(self, caller, context): context = context.clone() context.boundnode = self.bound - return self._proxied.infer_call_result(caller, context) + return super(BoundMethod, self).infer_call_result(caller, context) def bool_value(self): return True @@ -407,7 +343,7 @@ def path_wrapper(func): def wrapped(node, context=None, _func=func, **kwargs): """wrapper function handling context""" if context is None: - context = InferenceContext() + context = contextmod.InferenceContext() context.push(node) yielded = set() for res in _func(node, context, **kwargs): @@ -416,7 +352,7 @@ def path_wrapper(func): ares = res._proxied else: ares = res - if not ares in yielded: + if ares not in yielded: yield res yielded.add(ares) return wrapped @@ -428,7 +364,7 @@ def yes_if_nothing_inferred(func): inferred = True yield node if not inferred: - yield YES + yield util.YES return wrapper def raise_if_nothing_inferred(func): @@ -438,7 +374,7 @@ def raise_if_nothing_inferred(func): inferred = True yield node if not inferred: - raise InferenceError() + raise exceptions.InferenceError() return wrapper @@ -479,7 +415,7 @@ class NodeNG(object): try: # pylint: disable=not-callable return self._explicit_inference(self, context, **kwargs) - except UseInferenceDefault: + except exceptions.UseInferenceDefault: pass if not context: @@ -578,7 +514,7 @@ class NodeNG(object): return node_or_sequence msg = 'Could not find %s in %s\'s children' - raise AstroidError(msg % (repr(child), repr(self))) + raise exceptions.AstroidError(msg % (repr(child), repr(self))) def locate_child(self, child): """return a 2-uple (child attribute name, sequence or node)""" @@ -590,7 +526,7 @@ class NodeNG(object): if isinstance(node_or_sequence, (tuple, list)) and child in node_or_sequence: return field, node_or_sequence msg = 'Could not find %s in %s\'s children' - raise AstroidError(msg % (repr(child), repr(self))) + raise exceptions.AstroidError(msg % (repr(child), repr(self))) # FIXME : should we merge child_sequence and locate_child ? locate_child # is only used in are_exclusive, child_sequence one time in pylint. @@ -623,14 +559,14 @@ class NodeNG(object): # these are lazy because they're relatively expensive to compute for every # single node, and they rarely get looked at - @cachedproperty + @decoratorsmod.cachedproperty def fromlineno(self): if self.lineno is None: return self._fixed_source_line() else: return self.lineno - @cachedproperty + @decoratorsmod.cachedproperty def tolineno(self): if not self._astroid_fields: # can't have children @@ -694,7 +630,7 @@ class NodeNG(object): def _infer(self, context=None): """we don't know how to resolve a statement by default""" # this method is overridden by most concrete classes - raise InferenceError(self.__class__.__name__) + raise exceptions.InferenceError(self.__class__.__name__) def inferred(self): '''return list of inferred values for a more simple inference usage''' @@ -742,7 +678,7 @@ class NodeNG(object): * YES: the inference engine is uncertain of the node's value. """ - return YES + return util.YES class Statement(NodeNG): diff --git a/astroid/brain/builtin_inference.py b/astroid/brain/builtin_inference.py index 0d713845..33f086f0 100644 --- a/astroid/brain/builtin_inference.py +++ b/astroid/brain/builtin_inference.py @@ -1,15 +1,18 @@ """Astroid hooks for various builtins.""" + from functools import partial import sys from textwrap import dedent import six from astroid import (MANAGER, UseInferenceDefault, NotFoundError, - inference_tip, YES, InferenceError, UnresolvableName) + inference_tip, InferenceError, UnresolvableName) from astroid.builder import AstroidBuilder from astroid import helpers from astroid import nodes from astroid import objects +from astroid import scoped_nodes +from astroid import util def _extend_str(class_node, rvalue): """function to extend builtin str/unicode class""" @@ -116,10 +119,10 @@ def _generic_inference(node, context, node_type, transform): inferred = next(arg.infer(context=context)) except (InferenceError, StopIteration): raise UseInferenceDefault() - if inferred is YES: + if infered is util.YES: raise UseInferenceDefault() transformed = transform(inferred) - if not transformed or transformed is YES: + if not transformed or transformed is util.YES: raise UseInferenceDefault() return transformed @@ -226,19 +229,16 @@ def infer_dict(node, context=None): If a case can't be inferred, we'll fallback to default inference. """ - has_keywords = lambda args: all(isinstance(arg, nodes.Keyword) - for arg in args) - if not node.args and not node.kwargs: + if not node.args and not node.kwargs and not node.keywords: # dict() return nodes.Dict() - elif has_keywords(node.args) and node.args: + elif node.keywords and not node.args: # dict(a=1, b=2, c=4) - items = [(nodes.Const(arg.arg), arg.value) for arg in node.args] - elif (len(node.args) >= 2 and - has_keywords(node.args[1:])): + items = [(nodes.Const(arg.arg), arg.value) for arg in node.keywords] + elif len(node.args) == 1 and node.keywords: # dict(some_iterable, b=2, c=4) elts = _get_elts(node.args[0], context) - keys = [(nodes.Const(arg.arg), arg.value) for arg in node.args[1:]] + keys = [(nodes.Const(arg.arg), arg.value) for arg in node.keywords] items = elts + keys elif len(node.args) == 1: items = _get_elts(node.args[0], context) @@ -250,16 +250,6 @@ def infer_dict(node, context=None): return empty -def _node_class(node): - klass = node.frame() - while klass is not None and not isinstance(klass, nodes.ClassDef): - if klass.parent is None: - klass = None - else: - klass = klass.parent.frame() - return klass - - def infer_super(node, context=None): """Understand super calls. @@ -285,7 +275,7 @@ def infer_super(node, context=None): # Not interested in staticmethods. raise UseInferenceDefault - cls = _node_class(scope) + cls = scoped_nodes.get_wrapping_class(scope) if not len(node.args): mro_pointer = cls # In we are in a classmethod, the interpreter will fill @@ -305,7 +295,7 @@ def infer_super(node, context=None): except InferenceError: raise UseInferenceDefault - if mro_pointer is YES or mro_type is YES: + if mro_pointer is util.YES or mro_type is util.YES: # No way we could understand this. raise UseInferenceDefault @@ -329,11 +319,11 @@ def _infer_getattr_args(node, context): except InferenceError: raise UseInferenceDefault - if obj is YES or attr is YES: + if obj is util.YES or attr is util.YES: # If one of the arguments is something we can't infer, # then also make the result of the getattr call something # which is unknown. - return YES, YES + return util.YES, util.YES is_string = (isinstance(attr, nodes.Const) and isinstance(attr.value, six.string_types)) @@ -351,8 +341,8 @@ def infer_getattr(node, context=None): lookup will be done. """ obj, attr = _infer_getattr_args(node, context) - if obj is YES or attr is YES: - return YES + if obj is util.YES or attr is util.YES: + return util.YES try: return next(obj.igetattr(attr, context=context)) @@ -378,12 +368,12 @@ def infer_hasattr(node, context=None): """ try: obj, attr = _infer_getattr_args(node, context) - if obj is YES or attr is YES: - return YES + if obj is util.YES or attr is util.YES: + return util.YES obj.getattr(attr, context=context) except UseInferenceDefault: # Can't infer something from this function call. - return YES + return util.YES except NotFoundError: # Doesn't have it. return nodes.Const(False) @@ -406,9 +396,9 @@ def infer_callable(node, context=None): try: inferred = next(argument.infer(context=context)) except InferenceError: - return YES - if inferred is YES: - return YES + return util.YES + if inferred is util.YES: + return util.YES return nodes.Const(inferred.callable()) @@ -425,13 +415,13 @@ def infer_bool(node, context=None): try: inferred = next(argument.infer(context=context)) except InferenceError: - return YES - if inferred is YES: - return YES + return util.YES + if inferred is util.YES: + return util.YES bool_value = inferred.bool_value() - if bool_value is YES: - return YES + if bool_value is util.YES: + return util.YES return nodes.Const(bool_value) diff --git a/astroid/brain/py2stdlib.py b/astroid/brain/py2stdlib.py index b601a825..b742823c 100644 --- a/astroid/brain/py2stdlib.py +++ b/astroid/brain/py2stdlib.py @@ -12,10 +12,11 @@ from textwrap import dedent from astroid import ( MANAGER, AsStringRegexpPredicate, UseInferenceDefault, inference_tip, BoundMethod, - YES, InferenceError, register_module_extender) + InferenceError, register_module_extender) from astroid import exceptions from astroid import nodes from astroid.builder import AstroidBuilder +from astroid import util PY3K = sys.version_info > (3, 0) PY33 = sys.version_info >= (3, 3) @@ -28,7 +29,7 @@ def infer_func_form(node, base_type, context=None, enum=False): def infer_first(node): try: value = next(node.infer(context=context)) - if value is YES: + if value is util.YES: raise UseInferenceDefault() else: return value @@ -190,6 +191,7 @@ def cleanup_resources(force=False): def subprocess_transform(): if PY3K: communicate = (bytes('string', 'ascii'), bytes('string', 'ascii')) + communicate_signature = 'def communicate(self, input=None, timeout=None)' init = """ def __init__(self, args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, @@ -201,6 +203,7 @@ def subprocess_transform(): """ else: communicate = ('string', 'string') + communicate_signature = 'def communicate(self, input=None)' init = """ def __init__(self, args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, @@ -228,7 +231,7 @@ def subprocess_transform(): %(init)s - def communicate(self, input=None): + %(communicate_signature)s: return %(communicate)r %(wait_signature)s: return self.returncode @@ -243,6 +246,7 @@ def subprocess_transform(): %(ctx_manager)s ''' % {'init': init, 'communicate': communicate, + 'communicate_signature': communicate_signature, 'wait_signature': wait_signature, 'ctx_manager': ctx_manager}) return AstroidBuilder(MANAGER).string_build(code) diff --git a/astroid/builder.py b/astroid/builder.py index 8756fa42..fcd9892f 100644 --- a/astroid/builder.py +++ b/astroid/builder.py @@ -33,6 +33,7 @@ from astroid import manager from astroid import modutils from astroid import raw_building from astroid import rebuilder +from astroid import util def _parse(string): @@ -83,11 +84,19 @@ MANAGER = manager.AstroidManager() class AstroidBuilder(raw_building.InspectBuilder): - """Class for building an astroid tree from source code or from a live module.""" + """Class for building an astroid tree from source code or from a live module. - def __init__(self, manager=None): + The param *manager* specifies the manager class which should be used. + If no manager is given, then the default one will be used. The + param *apply_transforms* determines if the transforms should be + applied after the tree was built from source or from a live object, + by default being True. + """ + + def __init__(self, manager=None, apply_transforms=True): super(AstroidBuilder, self).__init__() self._manager = manager or MANAGER + self._apply_transforms = apply_transforms def module_build(self, module, modname=None): """Build an astroid from a living module instance.""" @@ -101,12 +110,10 @@ class AstroidBuilder(raw_building.InspectBuilder): # this is a built-in module # get a partial representation by introspection node = self.inspect_build(module, modname=modname, path=path) - # we have to handle transformation by ourselves since the rebuilder - # isn't called for builtin nodes - # - # XXX it's then only called for Module nodes, not for underlying - # nodes - node = self._manager.transform(node) + if self._apply_transforms: + # We have to handle transformation by ourselves since the + # rebuilder isn't called for builtin nodes + node = self._manager.visit_transforms(node) return node def file_build(self, path, modname=None): @@ -153,6 +160,10 @@ class AstroidBuilder(raw_building.InspectBuilder): # handle delayed assattr nodes for delayed in module._delayed_assattr: self.delayed_assattr(delayed) + + # Visit the transforms + if self._apply_transforms: + module = self._manager.visit_transforms(module) return module def _data_build(self, data, modname, path): @@ -206,7 +217,7 @@ class AstroidBuilder(raw_building.InspectBuilder): try: frame = node.frame() for inferred in node.expr.infer(): - if inferred is bases.YES: + if inferred is util.YES: continue try: if inferred.__class__ is bases.Instance: @@ -236,13 +247,17 @@ class AstroidBuilder(raw_building.InspectBuilder): pass -def parse(code, module_name='', path=None): +def parse(code, module_name='', path=None, apply_transforms=True): """Parses a source string in order to obtain an astroid AST from it :param str code: The code for the module. :param str module_name: The name for the module, if any :param str path: The path for the module + :param bool apply_transforms: + Apply the transforms for the give code. Use it if you + don't want the default transforms to be applied. """ code = textwrap.dedent(code) - return AstroidBuilder(MANAGER).string_build( - code, modname=module_name, path=path) + builder = AstroidBuilder(manager=MANAGER, + apply_transforms=apply_transforms) + return builder.string_build(code, modname=module_name, path=path) diff --git a/astroid/context.py b/astroid/context.py new file mode 100644 index 00000000..3e420d76 --- /dev/null +++ b/astroid/context.py @@ -0,0 +1,141 @@ +# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of astroid. +# +# astroid is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 2.1 of the License, or (at your +# option) any later version. +# +# astroid 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 Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with astroid. If not, see <http://www.gnu.org/licenses/>. + +"""Various context related utilities, including inference and call contexts.""" + +import contextlib +import itertools + +from astroid import exceptions +from astroid import util + + +class InferenceContext(object): + __slots__ = ('path', 'lookupname', 'callcontext', 'boundnode', 'infered') + + def __init__(self, path=None, infered=None): + self.path = path or set() + self.lookupname = None + self.callcontext = None + self.boundnode = None + self.infered = infered or {} + + def push(self, node): + name = self.lookupname + if (node, name) in self.path: + raise StopIteration() + self.path.add((node, name)) + + def clone(self): + # XXX copy lookupname/callcontext ? + clone = InferenceContext(self.path, infered=self.infered) + clone.callcontext = self.callcontext + clone.boundnode = self.boundnode + return clone + + def cache_generator(self, key, generator): + results = [] + for result in generator: + results.append(result) + yield result + + self.infered[key] = tuple(results) + return + + @contextlib.contextmanager + def restore_path(self): + path = set(self.path) + yield + self.path = path + + +class CallContext(object): + + def __init__(self, args, keywords=None, starargs=None, kwargs=None): + self.args = args + if keywords: + self.keywords = {arg.arg: arg.value for arg in keywords} + else: + self.keywords = {} + + self.starargs = starargs + self.kwargs = kwargs + + @staticmethod + def _infer_argument_container(container, key, context): + its = [] + for infered in container.infer(context=context): + if infered is util.YES: + its.append((util.YES,)) + continue + try: + its.append(infered.getitem(key, context).infer(context=context)) + except (exceptions.InferenceError, AttributeError): + its.append((util.YES,)) + except (IndexError, TypeError): + continue + if its: + return itertools.chain(*its) + + def infer_argument(self, funcnode, name, context, boundnode): + """infer a function argument value according to the call context""" + # 1. search in named keywords + try: + return self.keywords[name].infer(context) + except KeyError: + pass + + argindex = funcnode.args.find_argname(name)[0] + if argindex is not None: + # 2. first argument of instance/class method + if argindex == 0 and funcnode.type in ('method', 'classmethod'): + return iter((boundnode,)) + # if we have a method, extract one position + # from the index, so we'll take in account + # the extra parameter represented by `self` or `cls` + if funcnode.type in ('method', 'classmethod'): + argindex -= 1 + # 2. search arg index + try: + return self.args[argindex].infer(context) + except IndexError: + pass + # 3. search in *args (.starargs) + if self.starargs is not None: + its = self._infer_argument_container( + self.starargs, argindex, context) + if its: + return its + # 4. Search in **kwargs + if self.kwargs is not None: + its = self._infer_argument_container( + self.kwargs, name, context) + if its: + return its + # 5. return default value if any + try: + return funcnode.args.default_value(name).infer(context) + except exceptions.NoDefault: + raise exceptions.InferenceError(name) + + +def copy_context(context): + if context is not None: + return context.clone() + else: + return InferenceContext() diff --git a/astroid/decorators.py b/astroid/decorators.py new file mode 100644 index 00000000..4bc7ff17 --- /dev/null +++ b/astroid/decorators.py @@ -0,0 +1,79 @@ +# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of astroid. +# +# astroid is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 2.1 of the License, or (at your +# option) any later version. +# +# astroid 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 Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with astroid. If not, see <http://www.gnu.org/licenses/>. +# +# The code in this file was originally part of logilab-common, licensed under +# the same license. + +""" A few useful function/method decorators.""" + +import functools + + +def cached(func): + """Simple decorator to cache result of method calls without args.""" + + @functools.wraps(func) + def wrapped(wrapped_self): + cache = getattr(wrapped_self, '__cache', None) + if cache is None: + wrapped_self.__cache = cache = {} + try: + return cache[func] + except KeyError: + cache[func] = result = func(wrapped_self) + return result + + return wrapped + + +class cachedproperty(object): + """ Provides a cached property equivalent to the stacking of + @cached and @property, but more efficient. + + After first usage, the <property_name> becomes part of the object's + __dict__. Doing: + + del obj.<property_name> empties the cache. + + Idea taken from the pyramid_ framework and the mercurial_ project. + + .. _pyramid: http://pypi.python.org/pypi/pyramid + .. _mercurial: http://pypi.python.org/pypi/Mercurial + """ + __slots__ = ('wrapped',) + + def __init__(self, wrapped): + try: + wrapped.__name__ + except AttributeError: + raise TypeError('%s must have a __name__ attribute' % + wrapped) + self.wrapped = wrapped + + @property + def __doc__(self): + doc = getattr(self.wrapped, '__doc__', None) + return ('<wrapped by the cachedproperty decorator>%s' + % ('\n%s' % doc if doc else '')) + + def __get__(self, inst, objtype=None): + if inst is None: + return self + val = self.wrapped(inst) + setattr(inst, self.wrapped.__name__, val) + return val diff --git a/astroid/helpers.py b/astroid/helpers.py index c22bd9a1..26844082 100644 --- a/astroid/helpers.py +++ b/astroid/helpers.py @@ -23,10 +23,12 @@ Various helper utilities. import six from astroid import bases +from astroid import context as contextmod from astroid import exceptions from astroid import manager from astroid import raw_building from astroid import scoped_nodes +from astroid import util BUILTINS = six.moves.builtins.__name__ @@ -60,7 +62,7 @@ def _function_type(function, builtins): def _object_type(node, context=None): astroid_manager = manager.AstroidManager() builtins = astroid_manager.astroid_cache[BUILTINS] - context = context or bases.InferenceContext() + context = context or contextmod.InferenceContext() for inferred in node.infer(context=context): if isinstance(inferred, scoped_nodes.ClassDef): @@ -89,9 +91,9 @@ def object_type(node, context=None): try: types = set(_object_type(node, context)) except exceptions.InferenceError: - return bases.YES - if len(types) > 1: - return bases.YES + return util.YES + if len(types) > 1 or not types: + return util.YES return list(types)[0] @@ -124,7 +126,7 @@ def has_known_bases(klass, context=None): for base in klass.bases: result = safe_infer(base, context=context) # TODO: check for A->B->A->B pattern in class structure too? - if (not isinstance(result, scoped_nodes.ClassDef) or + if (not isinstance(result, scoped_nodes.Class) or result is klass or not has_known_bases(result, context=context)): klass._all_bases_known = False @@ -135,7 +137,7 @@ def has_known_bases(klass, context=None): def _type_check(type1, type2): if not all(map(has_known_bases, (type1, type2))): - return bases.YES + return util.YES if not all([type1.newstyle, type2.newstyle]): return False @@ -143,7 +145,7 @@ def _type_check(type1, type2): return type1 in type2.mro()[:-1] except exceptions.MroError: # The MRO is invalid. - return bases.YES + return util.YES def is_subtype(type1, type2): diff --git a/astroid/inference.py b/astroid/inference.py index 30e02288..05cbe5eb 100644 --- a/astroid/inference.py +++ b/astroid/inference.py @@ -22,111 +22,17 @@ import functools import itertools import operator +from astroid import bases +from astroid import context as contextmod +from astroid import exceptions from astroid import helpers +from astroid import manager from astroid import nodes from astroid import protocols -from astroid.manager import AstroidManager -from astroid.exceptions import ( - AstroidError, InferenceError, NoDefault, - NotFoundError, UnresolvableName, - UnaryOperationError, - BinaryOperationError, -) -from astroid.bases import (YES, Instance, InferenceContext, BoundMethod, - _infer_stmts, copy_context, path_wrapper, - raise_if_nothing_inferred, yes_if_nothing_inferred) - -MANAGER = AstroidManager() - - -class CallContext(object): - """when inferring a function call, this class is used to remember values - given as argument - """ - def __init__(self, args, starargs, dstarargs): - self.args = [] - self.nargs = {} - for arg in args: - if isinstance(arg, nodes.Keyword): - self.nargs[arg.arg] = arg.value - else: - self.args.append(arg) - self.starargs = starargs - self.dstarargs = dstarargs +from astroid import util - def infer_argument(self, funcnode, name, context): - """infer a function argument value according to the call context""" - # 1. search in named keywords - try: - return self.nargs[name].infer(context) - except KeyError: - # Function.args.args can be None in astroid (means that we don't have - # information on argnames) - argindex = funcnode.args.find_argname(name)[0] - if argindex is not None: - # 2. first argument of instance/class method - if argindex == 0 and funcnode.type in ('method', 'classmethod'): - if context.boundnode is not None: - boundnode = context.boundnode - else: - # XXX can do better ? - boundnode = funcnode.parent.frame() - if funcnode.type == 'method': - if not isinstance(boundnode, Instance): - boundnode = Instance(boundnode) - return iter((boundnode,)) - if funcnode.type == 'classmethod': - return iter((boundnode,)) - # if we have a method, extract one position - # from the index, so we'll take in account - # the extra parameter represented by `self` or `cls` - if funcnode.type in ('method', 'classmethod'): - argindex -= 1 - # 2. search arg index - try: - return self.args[argindex].infer(context) - except IndexError: - pass - # 3. search in *args (.starargs) - if self.starargs is not None: - its = [] - for inferred in self.starargs.infer(context): - if inferred is YES: - its.append((YES,)) - continue - try: - its.append(inferred.getitem(argindex, context).infer(context)) - except (InferenceError, AttributeError): - its.append((YES,)) - except (IndexError, TypeError): - continue - if its: - return itertools.chain(*its) - # 4. XXX search in **kwargs (.dstarargs) - if self.dstarargs is not None: - its = [] - for inferred in self.dstarargs.infer(context): - if inferred is YES: - its.append((YES,)) - continue - try: - its.append(inferred.getitem(name, context).infer(context)) - except (InferenceError, AttributeError): - its.append((YES,)) - except (IndexError, TypeError): - continue - if its: - return itertools.chain(*its) - # 5. */** argument, (Tuple or Dict) - if name == funcnode.args.vararg: - return iter((nodes.const_factory(()))) - if name == funcnode.args.kwarg: - return iter((nodes.const_factory({}))) - # 6. return default value if any - try: - return funcnode.args.default_value(name).infer(context) - except NoDefault: - raise InferenceError(name) + +MANAGER = manager.AstroidManager() # .infer method ############################################################### @@ -176,72 +82,82 @@ def infer_name(self, context=None): _, stmts = parent_function.lookup(self.name) if not stmts: - raise UnresolvableName(self.name) + raise exceptions.UnresolvableName(self.name) context = context.clone() context.lookupname = self.name - return _infer_stmts(stmts, context, frame) -nodes.Name._infer = path_wrapper(infer_name) + return bases._infer_stmts(stmts, context, frame) +nodes.Name._infer = bases.path_wrapper(infer_name) nodes.AssignName.infer_lhs = infer_name # won't work with a path wrapper +@bases.raise_if_nothing_infered +@bases.path_wrapper def infer_callfunc(self, context=None): """infer a CallFunc node by trying to guess what the function returns""" callcontext = context.clone() - callcontext.callcontext = CallContext(self.args, self.starargs, self.kwargs) + callcontext.callcontext = contextmod.CallContext(args=self.args, + keywords=self.keywords, + starargs=self.starargs, + kwargs=self.kwargs) callcontext.boundnode = None for callee in self.func.infer(context): - if callee is YES: + if callee is util.YES: yield callee continue try: if hasattr(callee, 'infer_call_result'): for inferred in callee.infer_call_result(self, callcontext): yield inferred - except InferenceError: + except exceptions.InferenceError: ## XXX log error ? continue -nodes.Call._infer = path_wrapper(raise_if_nothing_inferred(infer_callfunc)) +nodes.Call._infer = infer_callfunc +@bases.path_wrapper def infer_import(self, context=None, asname=True): """infer an Import node: return the imported module/object""" name = context.lookupname if name is None: - raise InferenceError() + raise exceptions.InferenceError() if asname: yield self.do_import_module(self.real_name(name)) else: yield self.do_import_module(name) -nodes.Import._infer = path_wrapper(infer_import) +nodes.Import._infer = infer_import + def infer_name_module(self, name): - context = InferenceContext() + context = contextmod.InferenceContext() context.lookupname = name return self.infer(context, asname=False) nodes.Import.infer_name_module = infer_name_module +@bases.path_wrapper def infer_from(self, context=None, asname=True): """infer a From nodes: return the imported module/object""" name = context.lookupname if name is None: - raise InferenceError() + raise exceptions.InferenceError() if asname: name = self.real_name(name) module = self.do_import_module() try: - context = copy_context(context) + context = contextmod.copy_context(context) context.lookupname = name - return _infer_stmts(module.getattr(name, ignore_locals=module is self.root()), context) - except NotFoundError: - raise InferenceError(name) -nodes.ImportFrom._infer = path_wrapper(infer_from) + stmts = module.getattr(name, ignore_locals=module is self.root()) + return bases._infer_stmts(stmts, context) + except exceptions.NotFoundError: + raise exceptions.InferenceError(name) +nodes.ImportFrom._infer = infer_from +@bases.raise_if_nothing_infered def infer_getattr(self, context=None): """infer a Getattr node by using getattr on the associated object""" for owner in self.expr.infer(context): - if owner is YES: + if owner is util.YES: yield owner continue try: @@ -249,61 +165,63 @@ def infer_getattr(self, context=None): for obj in owner.igetattr(self.attrname, context): yield obj context.boundnode = None - except (NotFoundError, InferenceError): + except (exceptions.NotFoundError, exceptions.InferenceError): context.boundnode = None except AttributeError: # XXX method / function context.boundnode = None -nodes.Attribute._infer = path_wrapper(raise_if_nothing_inferred(infer_getattr)) -nodes.AssignAttr.infer_lhs = raise_if_nothing_inferred(infer_getattr) # # won't work with a path wrapper +nodes.Attribute._infer = bases.path_wrapper(infer_getattr) +nodes.AssignAttr.infer_lhs = infer_getattr # # won't work with a path wrapper +@bases.path_wrapper def infer_global(self, context=None): if context.lookupname is None: - raise InferenceError() + raise exceptions.InferenceError() try: - return _infer_stmts(self.root().getattr(context.lookupname), context) - except NotFoundError: - raise InferenceError() -nodes.Global._infer = path_wrapper(infer_global) + return bases._infer_stmts(self.root().getattr(context.lookupname), + context) + except exceptions.NotFoundError: + raise exceptions.InferenceError() +nodes.Global._infer = infer_global def infer_subscript(self, context=None): """infer simple subscription such as [1,2,3][0] or (1,2,3)[-1]""" value = next(self.value.infer(context)) - if value is YES: - yield YES + if value is util.YES: + yield util.YES return index = next(self.slice.infer(context)) - if index is YES: - yield YES + if index is util.YES: + yield util.YES return if isinstance(index, nodes.Const): try: assigned = value.getitem(index.value, context) except AttributeError: - raise InferenceError() + raise exceptions.InferenceError() except (IndexError, TypeError): - yield YES + yield util.YES return # Prevent inferring if the inferred subscript # is the same as the original subscripted object. - if self is assigned or assigned is YES: - yield YES + if self is assigned or assigned is util.YES: + yield util.YES return for inferred in assigned.infer(context): yield inferred else: - raise InferenceError() -nodes.Subscript._infer = path_wrapper(infer_subscript) -nodes.Subscript.infer_lhs = raise_if_nothing_inferred(infer_subscript) + raise exceptions.InferenceError() +nodes.Subscript._infer = bases.path_wrapper(infer_subscript) +nodes.Subscript.infer_lhs = bases.raise_if_nothing_infered(infer_subscript) -@raise_if_nothing_inferred -@path_wrapper +@bases.raise_if_nothing_inferred +@bases.path_wrapper def _infer_boolop(self, context=None): """Infer a boolean operation (and / or / not). @@ -312,30 +230,29 @@ def _infer_boolop(self, context=None): node. """ values = self.values - op = self.op + if self.op == 'or': + predicate = operator.truth + else: + predicate = operator.not_ + try: values = [value.infer(context=context) for value in values] - except InferenceError: - yield YES + except exceptions.InferenceError: + yield util.YES return for pair in itertools.product(*values): - if any(item is YES for item in pair): + if any(item is util.YES for item in pair): # Can't infer the final result, just yield YES. - yield YES + yield util.YES continue bool_values = [item.bool_value() for item in pair] - if any(item is YES for item in bool_values): + if any(item is util.YES for item in bool_values): # Can't infer the final result, just yield YES. - yield YES + yield util.YES continue - if op == 'or': - predicate = operator.truth - else: - predicate = operator.not_ - # Since the boolean operations are short circuited operations, # this code yields the first value for which the predicate is True # and if no value respected the predicate, then the last value will @@ -345,7 +262,7 @@ def _infer_boolop(self, context=None): # 0 and 1 -> 0 # 1 or 0 -> 1 # 0 or 1 -> 1 - value = YES + value = util.YES for value, bool_value in zip(pair, bool_values): if predicate(bool_value): yield value @@ -364,7 +281,7 @@ def _filter_operation_errors(self, infer_callable, context, error): # For the sake of .infer(), we don't care about operation # errors, which is the job of pylint. So return something # which shows that we can't infer the result. - yield YES + yield util.YES else: yield result @@ -376,7 +293,7 @@ def _infer_unaryop(self, context=None): yield operand.infer_unary_op(self.op) except TypeError as exc: # The operand doesn't support this operation. - yield UnaryOperationError(operand, self.op, exc) + yield exceptions.UnaryOperationError(operand, self.op, exc) except AttributeError as exc: meth = protocols.UNARY_OP_METHOD[self.op] if meth is None: @@ -384,15 +301,15 @@ def _infer_unaryop(self, context=None): # value and negate its result, unless it is # YES, which will be returned as is. bool_value = operand.bool_value() - if bool_value is not YES: + if bool_value is not util.YES: yield nodes.const_factory(not bool_value) else: - yield YES + yield util.YES else: - if not isinstance(operand, Instance): + if not isinstance(operand, bases.Instance): # The operation was used on something which # doesn't support it. - yield UnaryOperationError(operand, self.op, exc) + yield exceptions.UnaryOperationError(operand, self.op, exc) continue try: @@ -405,21 +322,21 @@ def _infer_unaryop(self, context=None): yield operand else: yield result - except NotFoundError as exc: + except exceptions.NotFoundError as exc: # The unary operation special method was not found. - yield UnaryOperationError(operand, self.op, exc) - except InferenceError: - yield YES + yield exceptions.UnaryOperationError(operand, self.op, exc) + except exceptions.InferenceError: + yield util.YES -@path_wrapper +@bases.path_wrapper def infer_unaryop(self, context=None): """Infer what an UnaryOp should return when evaluated.""" - return _filter_operation_errors(self, _infer_unaryop, - context, UnaryOperationError) + return _filter_operation_errors(self, _infer_unaryop, context, + exceptions.UnaryOperationError) nodes.UnaryOp._infer_unaryop = _infer_unaryop -nodes.UnaryOp._infer = raise_if_nothing_inferred(infer_unaryop) +nodes.UnaryOp._infer = bases.raise_if_nothing_inferred(infer_unaryop) def _is_not_implemented(const): @@ -433,6 +350,7 @@ def _invoke_binop_inference(instance, op, other, context, method_name): inferred = next(method.infer(context=context)) return instance.infer_binary_op(op, other, context, inferred) + def _aug_op(instance, op, other, context, reverse=False): """Get an inference callable for an augmented binary operation.""" method_name = protocols.AUGMENTED_OP_METHOD[op] @@ -442,6 +360,7 @@ def _aug_op(instance, op, other, context, reverse=False): context=context, method_name=method_name) + def _bin_op(instance, op, other, context, reverse=False): """Get an inference callable for a normal binary operation. @@ -469,11 +388,11 @@ def _get_binop_contexts(context, left, right): # left.__op__(right). for arg in (right, left): new_context = context.clone() - new_context.callcontext = CallContext( - [arg], starargs=None, dstarargs=None) + new_context.callcontext = contextmod.CallContext(args=[arg]) new_context.boundnode = None yield new_context + def _same_type(type1, type2): """Check if type1 is the same as type2.""" return type1.qname() == type2.qname() @@ -560,26 +479,26 @@ def _infer_binary_operation(left, right, op, context, flow_factory): results = list(method()) except AttributeError: continue - except NotFoundError: + except exceptions.NotFoundError: continue - except InferenceError: - yield YES + except exceptions.InferenceError: + yield util.YES return else: - if any(result is YES for result in results): - yield YES + if any(result is util.YES for result in results): + yield util.YES return # TODO(cpopa): since the inferrence engine might return # more values than are actually possible, we decide - # to return YES if we have union types. + # to return util.YES if we have union types. if all(map(_is_not_implemented, results)): continue not_implemented = sum(1 for result in results if _is_not_implemented(result)) if not_implemented and not_implemented != len(results): # Can't decide yet what this is, not yet though. - yield YES + yield util.YES return for result in results: @@ -587,21 +506,21 @@ def _infer_binary_operation(left, right, op, context, flow_factory): return # TODO(cpopa): yield a BinaryOperationError here, # since the operation is not supported - yield BinaryOperationError(left_type, op, right_type) + yield exceptions.BinaryOperationError(left_type, op, right_type) def _infer_binop(self, context): """Binary operation inferrence logic.""" if context is None: - context = InferenceContext() + context = contextmod.InferenceContext() left = self.left right = self.right op = self.op for lhs in left.infer(context=context): - if lhs is YES: + if lhs is util.YES: # Don't know how to process this. - yield YES + yield util.YES return # TODO(cpopa): if we have A() * A(), trying to infer @@ -611,9 +530,9 @@ def _infer_binop(self, context): rhs_context = context.clone() rhs_context.path = set() for rhs in right.infer(context=rhs_context): - if rhs is YES: + if rhs is util.YES: # Don't know how to process this. - yield YES + yield util.YES return results = _infer_binary_operation(lhs, rhs, op, @@ -622,25 +541,25 @@ def _infer_binop(self, context): yield result -@path_wrapper +@bases.path_wrapper def infer_binop(self, context=None): - return _filter_operation_errors(self, _infer_binop, - context, BinaryOperationError) + return _filter_operation_errors(self, _infer_binop, context, + exceptions.BinaryOperationError) nodes.BinOp._infer_binop = _infer_binop -nodes.BinOp._infer = yes_if_nothing_inferred(infer_binop) +nodes.BinOp._infer = bases.yes_if_nothing_inferred(infer_binop) def _infer_augassign(self, context=None): """Inferrence logic for augmented binary operations.""" if context is None: - context = InferenceContext() + context = contextmod.InferenceContext() op = self.op for lhs in self.target.infer_lhs(context=context): - if lhs is YES: + if lhs is util.YES: # Don't know how to process this. - yield YES + yield util.YES return # TODO(cpopa): if we have A() * A(), trying to infer @@ -650,9 +569,9 @@ def _infer_augassign(self, context=None): rhs_context = context.clone() rhs_context.path = set() for rhs in self.value.infer(context=rhs_context): - if rhs is YES: + if rhs is util.YES: # Don't know how to process this. - yield YES + yield util.YES return results = _infer_binary_operation(lhs, rhs, op, @@ -661,10 +580,10 @@ def _infer_augassign(self, context=None): yield result -@path_wrapper +@bases.path_wrapper def infer_augassign(self, context=None): - return _filter_operation_errors(self, _infer_augassign, - context, BinaryOperationError) + return _filter_operation_errors(self, _infer_augassign, context, + exceptions.BinaryOperationError) nodes.AugAssign._infer_augassign = _infer_augassign nodes.AugAssign._infer = infer_augassign @@ -675,11 +594,12 @@ nodes.AugAssign._infer = infer_augassign def infer_arguments(self, context=None): name = context.lookupname if name is None: - raise InferenceError() + raise exceptions.InferenceError() return protocols._arguments_infer_argname(self, name, context) nodes.Arguments._infer = infer_arguments +@bases.path_wrapper def infer_ass(self, context=None): """infer a AssName/AssAttr: need to inspect the RHS part of the assign node @@ -688,25 +608,25 @@ def infer_ass(self, context=None): if isinstance(stmt, nodes.AugAssign): return stmt.infer(context) stmts = list(self.assigned_stmts(context=context)) - return _infer_stmts(stmts, context) -nodes.AssignName._infer = path_wrapper(infer_ass) -nodes.AssignAttr._infer = path_wrapper(infer_ass) + return bases._infer_stmts(stmts, context) +nodes.AssignName._infer = infer_ass +nodes.AssignAttr._infer = infer_ass # no infer method on DelName and DelAttr (expected InferenceError) - +@bases.path_wrapper def infer_empty_node(self, context=None): if not self.has_underlying_object(): - yield YES + yield util.YES else: try: for inferred in MANAGER.infer_ast_from_something(self.object, - context=context): + context=context): yield inferred - except AstroidError: - yield YES -nodes.EmptyNode._infer = path_wrapper(infer_empty_node) + except exceptions.AstroidError: + yield util.YES +nodes.EmptyNode._infer = infer_empty_node def infer_index(self, context=None): @@ -722,20 +642,19 @@ def instance_getitem(self, index, context=None): if context: new_context = context.clone() else: - context = new_context = InferenceContext() + context = new_context = contextmod.InferenceContext() # Create a new callcontext for providing index as an argument. - new_context.callcontext = CallContext( - args=[index], starargs=None, dstarargs=None) + new_context.callcontext = contextmod.CallContext(args=[index]) new_context.boundnode = self method = next(self.igetattr('__getitem__', context=context)) - if not isinstance(method, BoundMethod): - raise InferenceError + if not isinstance(method, bases.BoundMethod): + raise exceptions.InferenceError try: return next(method.infer_call_result(self, new_context)) except StopIteration: - raise InferenceError + raise exceptions.InferenceError -Instance.getitem = instance_getitem +bases.Instance.getitem = instance_getitem diff --git a/astroid/manager.py b/astroid/manager.py index bbc790b9..ede213b0 100644 --- a/astroid/manager.py +++ b/astroid/manager.py @@ -19,16 +19,16 @@ possible by providing a class responsible to get astroid representation from various source and using a cache of built modules) """ -__docformat__ = "restructuredtext en" +from __future__ import print_function -import collections import imp import os import warnings import zipimport -from astroid.exceptions import AstroidBuildingException +from astroid import exceptions from astroid import modutils +from astroid import transforms def safe_repr(obj): @@ -54,11 +54,19 @@ class AstroidManager(object): # NOTE: cache entries are added by the [re]builder self.astroid_cache = {} self._mod_file_cache = {} - self.transforms = collections.defaultdict(list) self._failed_import_hooks = [] self.always_load_extensions = False self.optimize_ast = False self.extension_package_whitelist = set() + self._transform = transforms.TransformVisitor() + + # Export these APIs for convenience + self.register_transform = self._transform.register_transform + self.unregister_transform = self._transform.unregister_transform + + def visit_transforms(self, node): + """Visit the transforms and apply them to the given *node*.""" + return self._transform.visit(node) def ast_from_file(self, filepath, modname=None, fallback=True, source=False): """given a module name, return the astroid object""" @@ -79,8 +87,8 @@ class AstroidManager(object): return AstroidBuilder(self).file_build(filepath, modname) elif fallback and modname: return self.ast_from_module_name(modname) - raise AstroidBuildingException('unable to get astroid for file %s' % - filepath) + raise exceptions.AstroidBuildingException( + 'unable to get astroid for file %s' % filepath) def _build_stub_module(self, modname): from astroid.builder import AstroidBuilder @@ -118,18 +126,20 @@ class AstroidManager(object): module = modutils.load_module_from_name(modname) except Exception as ex: msg = 'Unable to load module %s (%s)' % (modname, ex) - raise AstroidBuildingException(msg) + raise exceptions.AstroidBuildingException(msg) return self.ast_from_module(module, modname) elif mp_type == imp.PY_COMPILED: - raise AstroidBuildingException("Unable to load compiled module %s" % (modname,)) + msg = "Unable to load compiled module %s" % (modname,) + raise exceptions.AstroidBuildingException(msg) if filepath is None: - raise AstroidBuildingException("Unable to load module %s" % (modname,)) + msg = "Unable to load module %s" % (modname,) + raise exceptions.AstroidBuildingException(msg) return self.ast_from_file(filepath, modname, fallback=False) - except AstroidBuildingException as e: + except exceptions.AstroidBuildingException as e: for hook in self._failed_import_hooks: try: return hook(modname) - except AstroidBuildingException: + except exceptions.AstroidBuildingException: pass raise e finally: @@ -166,9 +176,9 @@ class AstroidManager(object): modname.split('.'), context_file=contextfile) except ImportError as ex: msg = 'Unable to load module %s (%s)' % (modname, ex) - value = AstroidBuildingException(msg) + value = exceptions.AstroidBuildingException(msg) self._mod_file_cache[(modname, contextfile)] = value - if isinstance(value, AstroidBuildingException): + if isinstance(value, exceptions.AstroidBuildingException): raise value return value @@ -193,8 +203,8 @@ class AstroidManager(object): try: modname = klass.__module__ except AttributeError: - raise AstroidBuildingException( - 'Unable to get module for class %s' % safe_repr(klass)) + msg = 'Unable to get module for class %s' % safe_repr(klass) + raise exceptions.AstroidBuildingException(msg) modastroid = self.ast_from_module_name(modname) return modastroid.getattr(klass.__name__)[0] # XXX @@ -207,44 +217,30 @@ class AstroidManager(object): try: modname = klass.__module__ except AttributeError: - raise AstroidBuildingException( - 'Unable to get module for %s' % safe_repr(klass)) + msg = 'Unable to get module for %s' % safe_repr(klass) + raise exceptions.AstroidBuildingException(msg) except Exception as ex: - raise AstroidBuildingException( - 'Unexpected error while retrieving module for %s: %s' - % (safe_repr(klass), ex)) + msg = ('Unexpected error while retrieving module for %s: %s' + % (safe_repr(klass), ex)) + raise exceptions.AstroidBuildingException(msg) try: name = klass.__name__ except AttributeError: - raise AstroidBuildingException( - 'Unable to get name for %s' % safe_repr(klass)) + msg = 'Unable to get name for %s' % safe_repr(klass) + raise exceptions.AstroidBuildingException(msg) except Exception as ex: - raise AstroidBuildingException( - 'Unexpected error while retrieving name for %s: %s' - % (safe_repr(klass), ex)) + exc = ('Unexpected error while retrieving name for %s: %s' + % (safe_repr(klass), ex)) + raise exceptions.AstroidBuildingException(exc) # take care, on living object __module__ is regularly wrong :( modastroid = self.ast_from_module_name(modname) if klass is obj: - for inferred in modastroid.igetattr(name, context): + for inferred in modastroid.igetattr(name, context): yield inferred else: for inferred in modastroid.igetattr(name, context): yield inferred.instanciate_class() - def register_transform(self, node_class, transform, predicate=None): - """Register `transform(node)` function to be applied on the given - Astroid's `node_class` if `predicate` is None or returns true - when called with the node as argument. - - The transform function may return a value which is then used to - substitute the original node in the tree. - """ - self.transforms[node_class].append((transform, predicate)) - - def unregister_transform(self, node_class, transform, predicate=None): - """Unregister the given transform.""" - self.transforms[node_class].remove((transform, predicate)) - def register_failed_import_hook(self, hook): """Registers a hook to resolve imports that cannot be found otherwise. @@ -255,30 +251,6 @@ class AstroidManager(object): """ self._failed_import_hooks.append(hook) - def transform(self, node): - """Call matching transforms for the given node if any and return the - transformed node. - """ - cls = node.__class__ - if cls not in self.transforms: - # no transform registered for this class of node - return node - - transforms = self.transforms[cls] - orig_node = node # copy the reference - for transform_func, predicate in transforms: - if predicate is None or predicate(node): - ret = transform_func(node) - # if the transformation function returns something, it's - # expected to be a replacement for the node - if ret is not None: - if node is not orig_node: - # node has already be modified by some previous - # transformation, warn about it - warnings.warn('node %s substituted multiple times' % node) - node = ret - return node - def cache_module(self, module): """Cache a module if no module with the same name is known yet.""" self.astroid_cache.setdefault(module.name, module) diff --git a/astroid/mixins.py b/astroid/mixins.py index ec7b7b69..87834f10 100644 --- a/astroid/mixins.py +++ b/astroid/mixins.py @@ -20,16 +20,14 @@ import warnings -from logilab.common.decorators import cachedproperty - -from astroid.exceptions import (AstroidBuildingException, InferenceError, - NotFoundError) +from astroid import decorators +from astroid import exceptions class BlockRangeMixIn(object): """override block range """ - @cachedproperty + @decorators.cachedproperty def blockstart_tolineno(self): return self.lineno @@ -129,12 +127,12 @@ class ImportFromMixin(FilterStmtsMixin): try: return mymodule.import_module(modname, level=level, relative_only=level and level >= 1) - except AstroidBuildingException as ex: + except exceptions.AstroidBuildingException as ex: if isinstance(ex.args[0], SyntaxError): - raise InferenceError(str(ex)) - raise InferenceError(modname) + raise exceptions.InferenceError(str(ex)) + raise exceptions.InferenceError(modname) except SyntaxError as ex: - raise InferenceError(str(ex)) + raise exceptions.InferenceError(str(ex)) def real_name(self, asname): """get name from 'as' name""" @@ -146,5 +144,4 @@ class ImportFromMixin(FilterStmtsMixin): _asname = name if asname == _asname: return name - raise NotFoundError(asname) - + raise exceptions.NotFoundError(asname) diff --git a/astroid/modutils.py b/astroid/modutils.py index 5a6c3471..3798c2fa 100644 --- a/astroid/modutils.py +++ b/astroid/modutils.py @@ -28,8 +28,6 @@ """ from __future__ import with_statement -__docformat__ = "restructuredtext en" - import imp import os import platform @@ -43,8 +41,6 @@ try: except ImportError: pkg_resources = None -from logilab.common import _handle_blacklist - PY_ZIPMODULE = object() if sys.platform.startswith('win'): @@ -111,6 +107,18 @@ def _path_from_filename(filename, is_jython=IS_JYTHON): return filename +def _handle_blacklist(blacklist, dirnames, filenames): + """remove files/directories in the black list + + dirnames/filenames are usually from os.walk + """ + for norecurs in blacklist: + if norecurs in dirnames: + dirnames.remove(norecurs) + elif norecurs in filenames: + filenames.remove(norecurs) + + _NORM_PATH_CACHE = {} def _cache_normalize_path(path): @@ -337,8 +345,8 @@ def file_info_from_modpath(modpath, path=None, context_file=None): def get_module_part(dotted_name, context_file=None): """given a dotted name return the module part of the name : - >>> get_module_part('logilab.common.modutils.get_module_part') - 'logilab.common.modutils' + >>> get_module_part('astroid.as_string.dump') + 'astroid.as_string' :type dotted_name: str :param dotted_name: full name of the identifier we are interested in @@ -393,7 +401,7 @@ def get_module_part(dotted_name, context_file=None): return dotted_name -def get_module_files(src_directory, blacklist): +def get_module_files(src_directory, blacklist, list_all=False): """given a package directory return a list of all available python module's files in the package and its subpackages @@ -402,9 +410,12 @@ def get_module_files(src_directory, blacklist): path of the directory corresponding to the package :type blacklist: list or tuple - :param blacklist: - optional list of files or directory to ignore, default to the value of - `logilab.common.STD_BLACKLIST` + :param blacklist: iterable + list of files or directories to ignore. + + :type list_all: bool + :param list_all: + get files from all paths, including ones without __init__.py :rtype: list :return: @@ -415,7 +426,7 @@ def get_module_files(src_directory, blacklist): for directory, dirnames, filenames in os.walk(src_directory): _handle_blacklist(blacklist, dirnames, filenames) # check for __init__.py - if not '__init__.py' in filenames: + if not list_all and not '__init__.py' in filenames: dirnames[:] = () continue for filename in filenames: diff --git a/astroid/node_classes.py b/astroid/node_classes.py index 85f57dc9..1c25fac1 100644 --- a/astroid/node_classes.py +++ b/astroid/node_classes.py @@ -21,16 +21,15 @@ import lazy_object_proxy import six -from logilab.common.decorators import cachedproperty +from astroid import bases +from astroid import context as contextmod +from astroid import decorators +from astroid import exceptions +from astroid import mixins +from astroid import util -from astroid.exceptions import ( - NoDefault, UnaryOperationError, - InferenceError, BinaryOperationError -) -from astroid.bases import (NodeNG, Statement, Instance, InferenceContext, - _infer_stmts, YES, BUILTINS) -from astroid.mixins import (BlockRangeMixIn, AssignTypeMixin, - ParentAssignTypeMixin, ImportFromMixin) + +BUILTINS = six.moves.builtins.__name__ def unpack_infer(stmt, context=None): @@ -49,7 +48,7 @@ def unpack_infer(stmt, context=None): return # else, infer recursivly, except YES object that should be returned as is for inferred in stmt.infer(context): - if inferred is YES: + if inferred is util.YES: yield inferred else: for inf_inf in unpack_infer(inferred, context): @@ -133,8 +132,8 @@ class LookupMixIn(object): the lookup method """ frame, stmts = self.lookup(name) - context = InferenceContext() - return _infer_stmts(stmts, context, frame) + context = contextmod.InferenceContext() + return bases._infer_stmts(stmts, context, frame) def _filter_stmts(self, stmts, frame, offset): """filter statements to remove ignorable statements. @@ -258,7 +257,7 @@ class LookupMixIn(object): # Name classes -class AssignName(LookupMixIn, ParentAssignTypeMixin, NodeNG): +class AssignName(LookupMixIn, mixins.ParentAssignTypeMixin, bases.NodeNG): """class representing an AssignName node""" _other_fields = ('name',) @@ -267,7 +266,7 @@ class AssignName(LookupMixIn, ParentAssignTypeMixin, NodeNG): super(AssignName, self).__init__(lineno, col_offset, parent) -class DelName(LookupMixIn, ParentAssignTypeMixin, NodeNG): +class DelName(LookupMixIn, mixins.ParentAssignTypeMixin, bases.NodeNG): """class representing a DelName node""" _other_fields = ('name',) @@ -276,7 +275,7 @@ class DelName(LookupMixIn, ParentAssignTypeMixin, NodeNG): super(DelName, self).__init__(lineno, col_offset, parent) -class Name(LookupMixIn, NodeNG): +class Name(LookupMixIn, bases.NodeNG): """class representing a Name node""" _other_fields = ('name',) @@ -285,7 +284,7 @@ class Name(LookupMixIn, NodeNG): super(Name, self).__init__(lineno, col_offset, parent) -class Arguments(NodeNG, AssignTypeMixin): +class Arguments(mixins.AssignTypeMixin, bases.NodeNG): """class representing an Arguments node""" if six.PY3: # Python 3.4+ uses a different approach regarding annotations, @@ -338,7 +337,7 @@ class Arguments(NodeNG, AssignTypeMixin): return name return None - @cachedproperty + @decorators.cachedproperty def fromlineno(self): lineno = super(Arguments, self).fromlineno return max(lineno, self.parent.fromlineno or 0) @@ -374,7 +373,7 @@ class Arguments(NodeNG, AssignTypeMixin): i = _find_arg(argname, self.kwonlyargs)[0] if i is not None and self.kw_defaults[i] is not None: return self.kw_defaults[i] - raise NoDefault() + raise exceptions.NoDefault() def is_argument(self, name): """return True if the name is defined in arguments""" @@ -433,7 +432,7 @@ def _format_args(args, annotations=None, defaults=None): return ', '.join(values) -class AssignAttr(NodeNG, ParentAssignTypeMixin): +class AssignAttr(mixins.ParentAssignTypeMixin, bases.NodeNG): """class representing an AssignAttr node""" _astroid_fields = ('expr',) _other_fields = ('attrname',) @@ -447,7 +446,7 @@ class AssignAttr(NodeNG, ParentAssignTypeMixin): self.expr = expr -class Assert(Statement): +class Assert(bases.Statement): """class representing an Assert node""" _astroid_fields = ('test', 'fail',) test = None @@ -458,7 +457,7 @@ class Assert(Statement): self.test = test -class Assign(Statement, AssignTypeMixin): +class Assign(mixins.Statement, bases.AssignTypeMixin): """class representing an Assign node""" _astroid_fields = ('targets', 'value',) targets = None @@ -469,7 +468,7 @@ class Assign(Statement, AssignTypeMixin): self.value = value -class AugAssign(Statement, AssignTypeMixin): +class AugAssign(mixins.AssignTypeMixin, bases.Statement): """class representing an AugAssign node""" _astroid_fields = ('target', 'value') _other_fields = ('op',) @@ -497,12 +496,12 @@ class AugAssign(Statement, AssignTypeMixin): try: results = self._infer_augassign(context=context) return [result for result in results - if isinstance(result, BinaryOperationError)] - except InferenceError: + if isinstance(result, exceptions.BinaryOperationError)] + except exceptions.InferenceError: return [] -class Repr(NodeNG): +class Repr(bases.NodeNG): """class representing a Repr node""" _astroid_fields = ('value',) value = None @@ -511,7 +510,7 @@ class Repr(NodeNG): self.value = value -class BinOp(NodeNG): +class BinOp(bases.NodeNG): """class representing a BinOp node""" _astroid_fields = ('left', 'right') _other_fields = ('op',) @@ -539,12 +538,12 @@ class BinOp(NodeNG): try: results = self._infer_binop(context=context) return [result for result in results - if isinstance(result, BinaryOperationError)] - except InferenceError: + if isinstance(result, exceptions.BinaryOperationError)] + except exceptions.InferenceError: return [] -class BoolOp(NodeNG): +class BoolOp(bases.NodeNG): """class representing a BoolOp node""" _astroid_fields = ('values',) _other_fields = ('op',) @@ -558,15 +557,16 @@ class BoolOp(NodeNG): self.values = values -class Break(Statement): +class Break(bases.Statement): """class representing a Break node""" -class Call(NodeNG): +class Call(bases.NodeNG): """class representing a Call node""" - _astroid_fields = ('func', 'args', 'starargs', 'kwargs') + _astroid_fields = ('func', 'args', 'keywords', 'starargs', 'kwargs') func = None args = None + keywords = None starargs = None kwargs = None @@ -577,7 +577,7 @@ class Call(NodeNG): self.kwargs = kwargs -class Compare(NodeNG): +class Compare(bases.NodeNG): """class representing a Compare node""" _astroid_fields = ('left', 'ops',) left = None @@ -600,7 +600,7 @@ class Compare(NodeNG): #return self.left -class Comprehension(NodeNG): +class Comprehension(bases.NodeNG): """class representing a Comprehension node""" _astroid_fields = ('target', 'iter', 'ifs') target = None @@ -619,6 +619,13 @@ class Comprehension(NodeNG): def assign_type(self): return self + def ass_type(self): + warnings.warn('%s.ass_type() is deprecated, ' + 'use %s.assign_type() instead.' + % (type(self).__name__, type(self).__name__), + PendingDeprecationWarning) + return self.assign_type() + def _get_filtered_stmts(self, lookup_node, node, stmts, mystmt): """method used in filter_stmts""" if self is mystmt: @@ -634,7 +641,7 @@ class Comprehension(NodeNG): return stmts, False -class Const(NodeNG, Instance): +class Const(bases.NodeNG, bases.Instance): """represent a constant node like num, str, bool, None, bytes""" _other_fields = ('value',) @@ -667,11 +674,11 @@ class Const(NodeNG, Instance): return bool(self.value) -class Continue(Statement): +class Continue(bases.Statement): """class representing a Continue node""" -class Decorators(NodeNG): +class Decorators(bases.NodeNG): """class representing a Decorators node""" _astroid_fields = ('nodes',) nodes = None @@ -684,7 +691,7 @@ class Decorators(NodeNG): return self.parent.parent.scope() -class DelAttr(NodeNG, ParentAssignTypeMixin): +class DelAttr(mixins.ParentAssignTypeMixin, bases.NodeNG): """class representing a DelAttr node""" _astroid_fields = ('expr',) _other_fields = ('attrname',) @@ -698,7 +705,7 @@ class DelAttr(NodeNG, ParentAssignTypeMixin): self.expr = expr -class Delete(Statement, AssignTypeMixin): +class Delete(mixins.AssignTypeMixin, bases.Statement): """class representing a Delete node""" _astroid_fields = ('targets',) targets = None @@ -707,7 +714,7 @@ class Delete(Statement, AssignTypeMixin): self.targets = targets -class Dict(NodeNG, Instance): +class Dict(bases.NodeNG, bases.Instance): """class representing a Dict node""" _astroid_fields = ('items',) @@ -750,7 +757,7 @@ class Dict(NodeNG, Instance): def getitem(self, lookup_key, context=None): for key, value in self.items: for inferredkey in key.infer(context): - if inferredkey is YES: + if inferredkey is util.YES: continue if isinstance(inferredkey, Const) \ and inferredkey.value == lookup_key: @@ -763,7 +770,7 @@ class Dict(NodeNG, Instance): return bool(self.items) -class Expr(Statement): +class Expr(bases.Statement): """class representing a Expr node""" _astroid_fields = ('value',) value = None @@ -772,18 +779,18 @@ class Expr(Statement): self.value = value -class Ellipsis(NodeNG): # pylint: disable=redefined-builtin +class Ellipsis(bases.NodeNG): # pylint: disable=redefined-builtin """class representing an Ellipsis node""" def bool_value(self): return True -class EmptyNode(NodeNG): +class EmptyNode(bases.NodeNG): """class representing an EmptyNode node""" -class ExceptHandler(Statement, AssignTypeMixin): +class ExceptHandler(mixins.AssignTypeMixin, bases.Statement): """class representing an ExceptHandler node""" _astroid_fields = ('type', 'name', 'body',) type = None @@ -795,7 +802,7 @@ class ExceptHandler(Statement, AssignTypeMixin): self.name = name self.body = body - @cachedproperty + @decorators.cachedproperty def blockstart_tolineno(self): if self.name: return self.name.tolineno @@ -812,7 +819,7 @@ class ExceptHandler(Statement, AssignTypeMixin): return True -class Exec(Statement): +class Exec(bases.Statement): """class representing an Exec node""" _astroid_fields = ('expr', 'globals', 'locals',) expr = None @@ -825,16 +832,23 @@ class Exec(Statement): self.locals = locals -class ExtSlice(NodeNG): +class ExtSlice(bases.NodeNG): """class representing an ExtSlice node""" _astroid_fields = ('dims',) dims = None +<<<<<<< variant A def postinit(self, dims=None): self.dims = dims class For(BlockRangeMixIn, AssignTypeMixin, Statement): +>>>>>>> variant B + +class For(mixins.BlockRangeMixIn, mixins.AssignTypeMixin, bases.Statement): +####### Ancestor +class For(BlockRangeMixIn, AssignTypeMixin, Statement): +======= end """class representing a For node""" _astroid_fields = ('target', 'iter', 'body', 'orelse',) target = None @@ -849,12 +863,12 @@ class For(BlockRangeMixIn, AssignTypeMixin, Statement): self.orelse = orelse optional_assign = True - @cachedproperty + @decorators.cachedproperty def blockstart_tolineno(self): return self.iter.tolineno -class ImportFrom(ImportFromMixin, Statement): +class ImportFrom(mixins.ImportFromMixin, bases.Statement): """class representing a ImportFrom node""" _other_fields = ('modname', 'names', 'level') @@ -866,7 +880,7 @@ class ImportFrom(ImportFromMixin, Statement): super(ImportFrom, self).__init__(lineno, col_offset, parent) -class Attribute(NodeNG): +class Attribute(bases.NodeNG): """class representing a Attribute node""" _astroid_fields = ('expr',) _other_fields = ('attrname') @@ -880,7 +894,7 @@ class Attribute(NodeNG): self.expr = expr -class Global(Statement): +class Global(bases.Statement): """class representing a Global node""" _other_fields = ('names',) @@ -892,7 +906,7 @@ class Global(Statement): return name -class If(BlockRangeMixIn, Statement): +class If(mixins.BlockRangeMixIn, bases.Statement): """class representing an If node""" _astroid_fields = ('test', 'body', 'orelse') test = None @@ -904,7 +918,7 @@ class If(BlockRangeMixIn, Statement): self.body = body self.orelse = orelse - @cachedproperty + @decorators.cachedproperty def blockstart_tolineno(self): return self.test.tolineno @@ -918,7 +932,7 @@ class If(BlockRangeMixIn, Statement): self.body[0].fromlineno - 1) -class IfExp(NodeNG): +class IfExp(bases.NodeNG): """class representing an IfExp node""" _astroid_fields = ('test', 'body', 'orelse') test = None @@ -931,7 +945,7 @@ class IfExp(NodeNG): self.orelse = orelse -class Import(ImportFromMixin, Statement): +class Import(mixins.ImportFromMixin, bases.Statement): """class representing an Import node""" _other_fields = ('names',) @@ -940,7 +954,7 @@ class Import(ImportFromMixin, Statement): super(Import, self).__init__(lineno, col_offset, parent) -class Index(NodeNG): +class Index(bases.NodeNG): """class representing an Index node""" _astroid_fields = ('value',) value = None @@ -949,7 +963,7 @@ class Index(NodeNG): self.value = value -class Keyword(NodeNG): +class Keyword(bases.NodeNG): """class representing a Keyword node""" _astroid_fields = ('value',) _other_fields = ('arg',) @@ -963,7 +977,7 @@ class Keyword(NodeNG): self.value = value -class List(NodeNG, Instance, ParentAssignTypeMixin): +class List(mixins.ParentAssignTypeMixin, bases.NodeNG, bases.Instance): """class representing a List node""" _astroid_fields = ('elts',) @@ -996,7 +1010,7 @@ class List(NodeNG, Instance, ParentAssignTypeMixin): return bool(self.elts) -class Nonlocal(Statement): +class Nonlocal(bases.Statement): """class representing a Nonlocal node""" _other_fields = ('names',) @@ -1008,11 +1022,11 @@ class Nonlocal(Statement): return name -class Pass(Statement): +class Pass(bases.Statement): """class representing a Pass node""" -class Print(Statement): +class Print(bases.Statement): """class representing a Print node""" _astroid_fields = ('dest', 'values',) dest = None @@ -1027,7 +1041,7 @@ class Print(Statement): self.values = values -class Raise(Statement): +class Raise(bases.Statement): """class representing a Raise node""" exc = None if six.PY2: @@ -1041,6 +1055,7 @@ class Raise(Statement): self.tback = tback else: _astroid_fields = ('exc', 'cause') + exc = None cause = None def postinit(self, exc=None, cause=None): @@ -1055,7 +1070,7 @@ class Raise(Statement): return True -class Return(Statement): +class Return(bases.Statement): """class representing a Return node""" _astroid_fields = ('value',) value = None @@ -1064,7 +1079,7 @@ class Return(Statement): self.value = value -class Set(NodeNG, Instance, ParentAssignTypeMixin): +class Set(mixins.ParentAssignTypeMixin, bases.NodeNG, bases.Instance): """class representing a Set node""" _astroid_fields = ('elts',) @@ -1094,7 +1109,7 @@ class Set(NodeNG, Instance, ParentAssignTypeMixin): return bool(self.elts) -class Slice(NodeNG): +class Slice(bases.NodeNG): """class representing a Slice node""" _astroid_fields = ('lower', 'upper', 'step') lower = None @@ -1107,7 +1122,7 @@ class Slice(NodeNG): self.step = step -class Starred(NodeNG, ParentAssignTypeMixin): +class Starred(mixins.ParentAssignTypeMixin, bases.NodeNG): """class representing a Starred node""" _astroid_fields = ('value',) value = None @@ -1116,7 +1131,7 @@ class Starred(NodeNG, ParentAssignTypeMixin): self.value = value -class Subscript(NodeNG): +class Subscript(bases.NodeNG): """class representing a Subscript node""" _astroid_fields = ('value', 'slice') value = None @@ -1127,7 +1142,7 @@ class Subscript(NodeNG): self.slice = slice -class TryExcept(BlockRangeMixIn, Statement): +class TryExcept(mixins.BlockRangeMixIn, bases.Statement): """class representing a TryExcept node""" _astroid_fields = ('body', 'handlers', 'orelse',) body = None @@ -1155,7 +1170,7 @@ class TryExcept(BlockRangeMixIn, Statement): return self._elsed_block_range(lineno, self.orelse, last) -class TryFinally(BlockRangeMixIn, Statement): +class TryFinally(mixins.BlockRangeMixIn, bases.Statement): """class representing a TryFinally node""" _astroid_fields = ('body', 'finalbody',) body = None @@ -1175,7 +1190,7 @@ class TryFinally(BlockRangeMixIn, Statement): return self._elsed_block_range(lineno, self.finalbody) -class Tuple(NodeNG, Instance, ParentAssignTypeMixin): +class Tuple(mixins.ParentAssignTypeMixin, bases.NodeNG, bases.Instance): """class representing a Tuple node""" _astroid_fields = ('elts',) @@ -1208,7 +1223,7 @@ class Tuple(NodeNG, Instance, ParentAssignTypeMixin): return bool(self.elts) -class UnaryOp(NodeNG): +class UnaryOp(bases.NodeNG): """class representing an UnaryOp node""" _astroid_fields = ('operand',) _other_fields = ('op',) @@ -1234,12 +1249,12 @@ class UnaryOp(NodeNG): try: results = self._infer_unaryop(context=context) return [result for result in results - if isinstance(result, UnaryOperationError)] - except InferenceError: + if isinstance(result, exceptions.UnaryOperationError)] + except exceptions.InferenceError: return [] -class While(BlockRangeMixIn, Statement): +class While(mixins.BlockRangeMixIn, bases.Statement): """class representing a While node""" _astroid_fields = ('test', 'body', 'orelse',) test = None @@ -1251,7 +1266,7 @@ class While(BlockRangeMixIn, Statement): self.body = body self.orelse = orelse - @cachedproperty + @decorators.cachedproperty def blockstart_tolineno(self): return self.test.tolineno @@ -1260,7 +1275,7 @@ class While(BlockRangeMixIn, Statement): return self. _elsed_block_range(lineno, self.orelse) -class With(BlockRangeMixIn, AssignTypeMixin, Statement): +class With(mixins.BlockRangeMixIn, mixins.AssignTypeMixin, bases.Statement): """class representing a With node""" _astroid_fields = ('items', 'body') items = None @@ -1270,7 +1285,7 @@ class With(BlockRangeMixIn, AssignTypeMixin, Statement): self.items = items self.body = body - @cachedproperty + @decorators.cachedproperty def blockstart_tolineno(self): return self.items[-1][0].tolineno @@ -1283,7 +1298,7 @@ class With(BlockRangeMixIn, AssignTypeMixin, Statement): yield elt -class Yield(NodeNG): +class Yield(bases.NodeNG): """class representing a Yield node""" _astroid_fields = ('value',) value = None @@ -1317,15 +1332,15 @@ def _update_const_classes(): CONST_CLS[kls] = Const _update_const_classes() + def const_factory(value): """return an astroid node for a python value""" - # XXX we should probably be stricter here and only consider stuff - # in CONST_CLS or do better treatment: in case where value is not - # in CONST_CLS, we should rather recall the builder on this value - # than returning an empty node (another option being that - # const_factory shouldn't be called with something not in - # CONST_CLS) - assert not isinstance(value, NodeNG) + # XXX we should probably be stricter here and only consider stuff in + # CONST_CLS or do better treatment: in case where value is not in CONST_CLS, + # we should rather recall the builder on this value than returning an empty + # node (another option being that const_factory shouldn't be called with something + # not in CONST_CLS) + assert not isinstance(value, bases.NodeNG) try: return CONST_CLS[value.__class__](value) except (KeyError, AttributeError): diff --git a/astroid/nodes.py b/astroid/nodes.py index 820a8d49..8bbd4f1b 100644 --- a/astroid/nodes.py +++ b/astroid/nodes.py @@ -36,8 +36,6 @@ on ImportFrom and Import : """ # pylint: disable=unused-import,redefined-builtin -__docformat__ = "restructuredtext en" - from astroid.node_classes import ( Arguments, AssignAttr, Assert, Assign, AssignName, AugAssign, Repr, BinOp, BoolOp, Break, Call, Compare, diff --git a/astroid/objects.py b/astroid/objects.py index 6f4c91c1..a78dcb41 100644 --- a/astroid/objects.py +++ b/astroid/objects.py @@ -26,24 +26,21 @@ leads to an inferred FrozenSet: """ -from logilab.common.decorators import cachedproperty import six +from astroid import bases +from astroid import decorators +from astroid import exceptions from astroid import MANAGER -from astroid.bases import ( - BUILTINS, NodeNG, Instance, _infer_stmts, - BoundMethod, -) -from astroid.exceptions import ( - SuperError, SuperArgumentTypeError, - NotFoundError, MroError -) -from astroid.node_classes import const_factory -from astroid.scoped_nodes import ClassDef, FunctionDef -from astroid.mixins import ParentAssignTypeMixin - - -class FrozenSet(NodeNG, Instance, ParentAssignTypeMixin): +from astroid import mixins +from astroid import node_classes +from astroid import scoped_nodes + + +BUILTINS = six.moves.builtins.__name__ + + +class FrozenSet(bases.NodeNG, bases.Instance, mixins.ParentAssignTypeMixin): """class representing a FrozenSet composite node""" _astroid_fields = ('elts',) @@ -63,7 +60,7 @@ class FrozenSet(NodeNG, Instance, ParentAssignTypeMixin): if elts is None: self.elts = [] else: - self.elts = [const_factory(e) for e in elts] + self.elts = [node_classes.const_factory(e) for e in elts] def pytype(self): return '%s.frozenset' % BUILTINS @@ -74,7 +71,7 @@ class FrozenSet(NodeNG, Instance, ParentAssignTypeMixin): def _infer(self, context=None): yield self - @cachedproperty + @decorators.cachedproperty def _proxied(self): builtins = MANAGER.astroid_cache[BUILTINS] return builtins.getattr('frozenset')[0] @@ -83,7 +80,7 @@ class FrozenSet(NodeNG, Instance, ParentAssignTypeMixin): return bool(self.elts) -class Super(NodeNG): +class Super(bases.NodeNG): """Proxy class over a super call. This class offers almost the same behaviour as Python's super, @@ -114,31 +111,34 @@ class Super(NodeNG): def super_mro(self): """Get the MRO which will be used to lookup attributes in this super.""" - if not isinstance(self.mro_pointer, ClassDef): - raise SuperArgumentTypeError("The first super argument must be type.") + if not isinstance(self.mro_pointer, scoped_nodes.ClassDef): + raise exceptions.SuperArgumentTypeError( + "The first super argument must be type.") - if isinstance(self.type, ClassDef): + if isinstance(self.type, scoped_nodes.ClassDef): # `super(type, type)`, most likely in a class method. self._class_based = True mro_type = self.type else: mro_type = getattr(self.type, '_proxied', None) - if not isinstance(mro_type, (Instance, ClassDef)): - raise SuperArgumentTypeError("super(type, obj): obj must be an " - "instance or subtype of type") + if not isinstance(mro_type, (bases.Instance, scoped_nodes.ClassDef)): + raise exceptions.SuperArgumentTypeError( + "super(type, obj): obj must be an " + "instance or subtype of type") if not mro_type.newstyle: - raise SuperError("Unable to call super on old-style classes.") + raise exceptions.SuperError("Unable to call super on old-style classes.") mro = mro_type.mro() if self.mro_pointer not in mro: - raise SuperArgumentTypeError("super(type, obj): obj must be an " - "instance or subtype of type") + raise exceptions.SuperArgumentTypeError( + "super(type, obj): obj must be an " + "instance or subtype of type") index = mro.index(self.mro_pointer) return mro[index + 1:] - @cachedproperty + @decorators.cachedproperty def _proxied(self): builtins = MANAGER.astroid_cache[BUILTINS] return builtins.getattr('super')[0] @@ -164,10 +164,10 @@ class Super(NodeNG): try: mro = self.super_mro() - except (MroError, SuperError) as exc: + except (exceptions.MroError, exceptions.SuperError) as exc: # Don't let invalid MROs or invalid super calls # to leak out as is from this function. - six.raise_from(NotFoundError, exc) + six.raise_from(exceptions.NotFoundError, exc) found = False for cls in mro: @@ -175,24 +175,24 @@ class Super(NodeNG): continue found = True - for inferred in _infer_stmts([cls[name]], context, frame=self): - if not isinstance(inferred, FunctionDef): + for inferred in bases._infer_stmts([cls[name]], context, frame=self): + if not isinstance(inferred, scoped_nodes.FunctionDef): yield inferred continue # We can obtain different descriptors from a super depending # on what we are accessing and where the super call is. if inferred.type == 'classmethod': - yield BoundMethod(inferred, cls) + yield bases.BoundMethod(inferred, cls) elif self._scope.type == 'classmethod' and inferred.type == 'method': yield inferred elif self._class_based or inferred.type == 'staticmethod': yield inferred else: - yield BoundMethod(inferred, cls) + yield bases.BoundMethod(inferred, cls) if not found: - raise NotFoundError(name) + raise exceptions.NotFoundError(name) def getattr(self, name, context=None): return list(self.igetattr(name, context=context)) diff --git a/astroid/protocols.py b/astroid/protocols.py index 3600840b..72f78984 100644 --- a/astroid/protocols.py +++ b/astroid/protocols.py @@ -25,16 +25,12 @@ import sys import six -from astroid.exceptions import InferenceError, NoDefault, NotFoundError -from astroid.node_classes import unpack_infer -from astroid.bases import ( - InferenceContext, copy_context, - raise_if_nothing_inferred, yes_if_nothing_inferred, - Instance, YES, BoundMethod, - Generator, -) -from astroid.nodes import const_factory +from astroid import bases +from astroid import context as contextmod +from astroid import exceptions +from astroid import node_classes from astroid import nodes +from astroid import util def _reflected_name(name): @@ -85,7 +81,7 @@ _UNARY_OPERATORS = { def _infer_unary_op(obj, op): func = _UNARY_OPERATORS[op] value = func(obj) - return const_factory(value) + return nodes.const_factory(value) nodes.Tuple.infer_unary_op = lambda self, op: _infer_unary_op(tuple(self.elts), op) nodes.List.infer_unary_op = lambda self, op: _infer_unary_op(self.elts, op) @@ -115,45 +111,48 @@ if sys.version_info >= (3, 5): for _KEY, _IMPL in list(BIN_OP_IMPL.items()): BIN_OP_IMPL[_KEY + '='] = _IMPL + +@bases.yes_if_nothing_inferred def const_infer_binary_op(self, operator, other, context, _): not_implemented = nodes.Const(NotImplemented) if isinstance(other, nodes.Const): try: impl = BIN_OP_IMPL[operator] try: - yield const_factory(impl(self.value, other.value)) + yield nodes.const_factory(impl(self.value, other.value)) except TypeError: # ArithmeticError is not enough: float >> float is a TypeError yield not_implemented except Exception: # pylint: disable=broad-except - yield YES + yield util.YES except TypeError: yield not_implemented elif isinstance(self.value, six.string_types) and operator == '%': # TODO(cpopa): implement string interpolation later on. - yield YES + yield util.YES else: yield not_implemented -nodes.Const.infer_binary_op = yes_if_nothing_inferred(const_infer_binary_op) +nodes.Const.infer_binary_op = const_infer_binary_op def _multiply_seq_by_int(self, other, context): node = self.__class__() elts = [n for elt in self.elts for n in elt.infer(context) - if not n is YES] * other.value + if not n is util.YES] * other.value node.elts = elts return node +@bases.yes_if_nothing_inferred def tl_infer_binary_op(self, operator, other, context, method): not_implemented = nodes.Const(NotImplemented) if isinstance(other, self.__class__) and operator == '+': node = self.__class__() elts = [n for elt in self.elts for n in elt.infer(context) - if not n is YES] + if not n is util.YES] elts += [n for elt in other.elts for n in elt.infer(context) - if not n is YES] + if not n is util.YES] node.elts = elts yield node elif isinstance(other, nodes.Const) and operator == '*': @@ -161,24 +160,25 @@ def tl_infer_binary_op(self, operator, other, context, method): yield not_implemented return yield _multiply_seq_by_int(self, other, context) - elif isinstance(other, Instance) and operator == '*': + elif isinstance(other, bases.Instance) and operator == '*': # Verify if the instance supports __index__. as_index = class_as_index(other, context) if not as_index: - yield YES + yield util.YES else: yield _multiply_seq_by_int(self, as_index, context) else: yield not_implemented -nodes.Tuple.infer_binary_op = yes_if_nothing_inferred(tl_infer_binary_op) -nodes.List.infer_binary_op = yes_if_nothing_inferred(tl_infer_binary_op) +nodes.Tuple.infer_binary_op = tl_infer_binary_op +nodes.List.infer_binary_op = tl_infer_binary_op +@bases.yes_if_nothing_inferred def instance_infer_binary_op(self, operator, other, context, method): return method.infer_call_result(self, context) -Instance.infer_binary_op = yes_if_nothing_inferred(instance_infer_binary_op) +bases.Instance.infer_binary_op = instance_infer_binary_op # assignment ################################################################## @@ -199,7 +199,7 @@ def _resolve_looppart(parts, asspath, context): asspath = asspath[:] index = asspath.pop(0) for part in parts: - if part is YES: + if part is util.YES: continue # XXX handle __iter__ and log potentially detected errors if not hasattr(part, 'itered'): @@ -219,7 +219,7 @@ def _resolve_looppart(parts, asspath, context): # we achieved to resolved the assignment path, # don't infer the last part yield assigned - elif assigned is YES: + elif assigned is util.YES: break else: # we are not yet on the last part of the path @@ -228,10 +228,11 @@ def _resolve_looppart(parts, asspath, context): for inferred in _resolve_looppart(assigned.infer(context), asspath, context): yield inferred - except InferenceError: + except exceptions.InferenceError: break +@bases.raise_if_nothing_inferred def for_assigned_stmts(self, node, context=None, asspath=None): if asspath is None: for lst in self.iter.infer(context): @@ -243,8 +244,8 @@ def for_assigned_stmts(self, node, context=None, asspath=None): asspath, context): yield inferred -nodes.For.assigned_stmts = raise_if_nothing_inferred(for_assigned_stmts) -nodes.Comprehension.assigned_stmts = raise_if_nothing_inferred(for_assigned_stmts) +nodes.For.assigned_stmts = for_assigned_stmts +nodes.Comprehension.assigned_stmts = for_assigned_stmts def mulass_assigned_stmts(self, node, context=None, asspath=None): @@ -252,6 +253,7 @@ def mulass_assigned_stmts(self, node, context=None, asspath=None): asspath = [] asspath.insert(0, self.elts.index(node)) return self.parent.assigned_stmts(self, context, asspath) + nodes.Tuple.assigned_stmts = mulass_assigned_stmts nodes.List.assigned_stmts = mulass_assigned_stmts @@ -266,57 +268,70 @@ def _arguments_infer_argname(self, name, context): # arguments information may be missing, in which case we can't do anything # more if not (self.args or self.vararg or self.kwarg): - yield YES + yield util.YES return # first argument of instance/class method if self.args and getattr(self.args[0], 'name', None) == name: functype = self.parent.type if functype == 'method': - yield Instance(self.parent.parent.frame()) + yield bases.Instance(self.parent.parent.frame()) return if functype == 'classmethod': yield self.parent.parent.frame() return + if name == self.vararg: - vararg = const_factory(()) + vararg = nodes.const_factory(()) vararg.parent = self yield vararg return if name == self.kwarg: - kwarg = const_factory({}) + kwarg = nodes.const_factory({}) kwarg.parent = self yield kwarg return # if there is a default value, yield it. And then yield YES to reflect # we can't guess given argument value try: - context = copy_context(context) + context = contextmod.copy_context(context) for inferred in self.default_value(name).infer(context): yield inferred - yield YES - except NoDefault: - yield YES + yield util.YES + except exceptions.NoDefault: + yield util.YES def arguments_assigned_stmts(self, node, context, asspath=None): if context.callcontext: # reset call context/name callcontext = context.callcontext - context = copy_context(context) + context = contextmod.copy_context(context) context.callcontext = None - return callcontext.infer_argument(self.parent, node.name, context) + # TODO(cpopa): make this an API + if context.boundnode is None: + boundnode = self.parent.parent.frame() + else: + boundnode = context.boundnode + if self.parent.type == 'method': + if not isinstance(boundnode, bases.Instance): + boundnode = bases.Instance(boundnode) + return callcontext.infer_argument( + self.parent, node.name, context, boundnode) return _arguments_infer_argname(self, node.name, context) + nodes.Arguments.assigned_stmts = arguments_assigned_stmts +@bases.raise_if_nothing_inferred def assign_assigned_stmts(self, node, context=None, asspath=None): if not asspath: yield self.value return for inferred in _resolve_asspart(self.value.infer(context), asspath, context): yield inferred -nodes.Assign.assigned_stmts = raise_if_nothing_inferred(assign_assigned_stmts) -nodes.AugAssign.assigned_stmts = raise_if_nothing_inferred(assign_assigned_stmts) + +nodes.Assign.assigned_stmts = assign_assigned_stmts +nodes.AugAssign.assigned_stmts = assign_assigned_stmts def _resolve_asspart(parts, asspath, context): @@ -335,7 +350,7 @@ def _resolve_asspart(parts, asspath, context): # we achieved to resolved the assignment path, don't infer the # last part yield assigned - elif assigned is YES: + elif assigned is util.YES: return else: # we are not yet on the last part of the path search on each @@ -344,24 +359,26 @@ def _resolve_asspart(parts, asspath, context): for inferred in _resolve_asspart(assigned.infer(context), asspath, context): yield inferred - except InferenceError: + except exceptions.InferenceError: return +@bases.raise_if_nothing_inferred def excepthandler_assigned_stmts(self, node, context=None, asspath=None): - for assigned in unpack_infer(self.type): + for assigned in node_classes.unpack_infer(self.type): if isinstance(assigned, nodes.ClassDef): - assigned = Instance(assigned) + assigned = bases.Instance(assigned) yield assigned -nodes.ExceptHandler.assigned_stmts = raise_if_nothing_inferred(excepthandler_assigned_stmts) + +nodes.ExceptHandler.assigned_stmts = excepthandler_assigned_stmts def _infer_context_manager(self, mgr, context): try: inferred = next(mgr.infer(context=context)) - except InferenceError: + except exceptions.InferenceError: return - if isinstance(inferred, Generator): + if isinstance(inferred, bases.Generator): # Check if it is decorated with contextlib.contextmanager. func = inferred.parent if not func.decorators: @@ -389,16 +406,18 @@ def _infer_context_manager(self, mgr, context): else: for inferred in yield_point.value.infer(context=context): yield inferred - elif isinstance(inferred, Instance): + elif isinstance(inferred, bases.Instance): try: enter = next(inferred.igetattr('__enter__', context=context)) - except (InferenceError, NotFoundError): + except (exceptions.InferenceError, exceptions.NotFoundError): return - if not isinstance(enter, BoundMethod): + if not isinstance(enter, bases.BoundMethod): return for result in enter.infer_call_result(self, context): yield result + +@bases.raise_if_nothing_inferred def with_assigned_stmts(self, node, context=None, asspath=None): """Infer names and other nodes from a *with* statement. @@ -428,22 +447,28 @@ def with_assigned_stmts(self, node, context=None, asspath=None): obj = result for index in asspath: if not hasattr(obj, 'elts'): - raise InferenceError + raise exceptions.InferenceError try: obj = obj.elts[index] except IndexError: - raise InferenceError + raise exceptions.InferenceError yield obj +<<<<<<< variant A nodes.With.assigned_stmts = raise_if_nothing_inferred(with_assigned_stmts) +>>>>>>> variant B +nodes.With.assigned_stmts = with_assigned_stmts +####### Ancestor +nodes.With.assigned_stmts = raise_if_nothing_infered(with_assigned_stmts) +======= end -@yes_if_nothing_inferred +@bases.yes_if_nothing_inferred def starred_assigned_stmts(self, node=None, context=None, asspath=None): stmt = self.statement() if not isinstance(stmt, (nodes.Assign, nodes.For)): - raise InferenceError() + raise exceptions.InferenceError() if isinstance(stmt, nodes.Assign): value = stmt.value @@ -451,24 +476,24 @@ def starred_assigned_stmts(self, node=None, context=None, asspath=None): if sum(1 for node in lhs.nodes_of_class(nodes.Starred)) > 1: # Too many starred arguments in the expression. - raise InferenceError() + raise exceptions.InferenceError() if context is None: - context = InferenceContext() + context = contextmod.InferenceContext() try: rhs = next(value.infer(context)) - except InferenceError: - yield YES + except exceptions.InferenceError: + yield util.YES return - if rhs is YES or not hasattr(rhs, 'elts'): + if rhs is util.YES or not hasattr(rhs, 'elts'): # Not interested in inferred values without elts. - yield YES + yield util.YES return elts = collections.deque(rhs.elts[:]) if len(lhs.elts) > len(rhs.elts): # a, *b, c = (1, 2) - raise InferenceError() + raise exceptions.InferenceError() # Unpack iteratively the values from the rhs of the assignment, # until the find the starred node. What will remain will @@ -504,13 +529,13 @@ def class_as_index(node, context): """ try: for inferred in node.igetattr('__index__', context=context): - if not isinstance(inferred, BoundMethod): + if not isinstance(inferred, bases.BoundMethod): continue for result in inferred.infer_call_result(node, context=context): if (isinstance(result, nodes.Const) and isinstance(result.value, int)): return result - except InferenceError: + except exceptions.InferenceError: pass -
\ No newline at end of file + diff --git a/astroid/raw_building.py b/astroid/raw_building.py index 42fc0853..ce80b0e2 100644 --- a/astroid/raw_building.py +++ b/astroid/raw_building.py @@ -19,26 +19,23 @@ (build_* functions) or from living object (object_build_* functions) """ -__docformat__ = "restructuredtext en" - -import sys +import inspect import logging import os -from os.path import abspath -from inspect import (getargspec, isdatadescriptor, isfunction, ismethod, - ismethoddescriptor, isclass, isbuiltin, ismodule, - isroutine) +import sys +import types + import six -from astroid.node_classes import CONST_CLS -from astroid.nodes import (Module, ClassDef, Const, const_factory, ImportFrom, - FunctionDef, EmptyNode, Name, Arguments) -from astroid.bases import BUILTINS, Generator -from astroid.manager import AstroidManager +from astroid import bases +from astroid import manager +from astroid import node_classes +from astroid import nodes -MANAGER = AstroidManager() -_CONSTANTS = tuple(CONST_CLS) # the keys of CONST_CLS eg python builtin types +MANAGER = manager.AstroidManager() +# the keys of CONST_CLS eg python builtin types +_CONSTANTS = tuple(node_classes.CONST_CLS) _JYTHON = os.name == 'java' _BUILTINS = vars(six.moves.builtins) _LOG = logging.getLogger(__name__) @@ -48,7 +45,7 @@ def _io_discrepancy(member): # _io module names itself `io`: http://bugs.python.org/issue18602 member_self = getattr(member, '__self__', None) return (member_self and - ismodule(member_self) and + inspect.ismodule(member_self) and member_self.__name__ == '_io' and member.__module__ == 'io') @@ -63,58 +60,63 @@ def attach_dummy_node(node, name, object=_marker): """create a dummy node and register it in the locals of the given node with the specified name """ - enode = EmptyNode() + enode = nodes.EmptyNode() enode.object = object _attach_local_node(node, enode, name) def _has_underlying_object(self): return hasattr(self, 'object') and self.object is not _marker -EmptyNode.has_underlying_object = _has_underlying_object +nodes.EmptyNode.has_underlying_object = _has_underlying_object 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 """ - if not name in node.special_attributes: - _attach_local_node(node, const_factory(value), name) + if name not in node.special_attributes: + _attach_local_node(node, nodes.const_factory(value), name) def attach_import_node(node, modname, membername): """create a ImportFrom node and register it in the locals of the given node with the specified name """ - from_node = ImportFrom(modname, [(membername, None)]) + from_node = nodes.ImportFrom(modname, [(membername, None)]) _attach_local_node(node, from_node, membername) def build_module(name, doc=None): """create and initialize a astroid Module node""" - return Module(name, doc, package=False, parent=None, pure_python=False) + node = nodes.Module(name, doc, pure_python=False) + node.package = False + node.parent = None + return node + def build_class(name, basenames=(), doc=None): """create and initialize a astroid ClassDef node""" node = ClassDef(name, doc) for base in basenames: - basenode = Name() + basenode = nodes.Name() basenode.name = base node.bases.append(basenode) basenode.parent = node return node + def build_function(name, args=None, defaults=None, flag=0, doc=None): """create and initialize a astroid FunctionDef node""" args, defaults = args or [], defaults or [] # first argument is now a list of decorators - func = FunctionDef(name, doc) - func.args = argsnode = Arguments() + func = nodes.FunctionDef(name, doc) + func.args = argsnode = nodes.Arguments() argsnode.args = [] for arg in args: - argsnode.args.append(Name()) + argsnode.args.append(nodes.Name()) argsnode.args[-1].name = arg argsnode.args[-1].parent = argsnode argsnode.defaults = [] for default in defaults: - argsnode.defaults.append(const_factory(default)) + argsnode.defaults.append(nodes.const_factory(default)) argsnode.defaults[-1].parent = argsnode argsnode.kwarg = None argsnode.vararg = None @@ -126,7 +128,7 @@ def build_function(name, args=None, defaults=None, flag=0, doc=None): def build_from_import(fromname, names): """create and initialize an astroid ImportFrom import statement""" - return ImportFrom(fromname, [(name, None) for name in names]) + return nodes.ImportFrom(fromname, [(name, None) for name in names]) def register_arguments(func, args=None): """add given arguments to local @@ -141,32 +143,37 @@ def register_arguments(func, args=None): if func.args.kwarg: func.set_local(func.args.kwarg, func.args) for arg in args: - if isinstance(arg, Name): + if isinstance(arg, nodes.Name): func.set_local(arg.name, arg) else: register_arguments(func, arg.elts) + def object_build_class(node, member, localname): """create astroid for a living class object""" basenames = [base.__name__ for base in member.__bases__] return _base_class_object_build(node, member, basenames, localname=localname) + def object_build_function(node, member, localname): """create astroid for a living function object""" - args, varargs, varkw, defaults = getargspec(member) + args, varargs, varkw, defaults = inspect.getargspec(member) if varargs is not None: args.append(varargs) if varkw is not None: args.append(varkw) func = build_function(getattr(member, '__name__', None) or localname, args, - defaults, six.get_function_code(member).co_flags, member.__doc__) + defaults, six.get_function_code(member).co_flags, + member.__doc__) node.add_local_node(func, localname) + def object_build_datadescriptor(node, member, name): """create astroid for a living data descriptor object""" return _base_class_object_build(node, member, [], name) + def object_build_methoddescriptor(node, member, localname): """create astroid for a living method descriptor object""" # FIXME get arguments ? @@ -177,6 +184,7 @@ def object_build_methoddescriptor(node, member, localname): func.args.args = None node.add_local_node(func, localname) + def _base_class_object_build(node, member, basenames, name=None, localname=None): """create astroid for a living class object, with a given set of base names (e.g. ancestors) @@ -198,7 +206,7 @@ def _base_class_object_build(node, member, basenames, name=None, localname=None) pass else: for name, obj in instdict.items(): - valnode = EmptyNode() + valnode = nodes.EmptyNode() valnode.object = obj valnode.parent = klass valnode.lineno = 1 @@ -250,7 +258,7 @@ class InspectBuilder(object): except AttributeError: # in jython, java modules have no __doc__ (see #109562) node = build_module(modname) - node.file = node.path = path and abspath(path) or path + node.file = node.path = path and os.path.abspath(path) or path node.name = modname MANAGER.cache_module(node) node.package = hasattr(module, '__path__') @@ -272,21 +280,21 @@ class InspectBuilder(object): # damned ExtensionClass.Base, I know you're there ! attach_dummy_node(node, name) continue - if ismethod(member): + if inspect.ismethod(member): member = six.get_method_function(member) - if isfunction(member): + if inspect.isfunction(member): _build_from_function(node, name, member, self._module) - elif isbuiltin(member): + elif inspect.isbuiltin(member): if (not _io_discrepancy(member) and self.imported_member(node, member, name)): continue object_build_methoddescriptor(node, member, name) - elif isclass(member): + elif inspect.isclass(member): if self.imported_member(node, member, name): continue if member in self._done: class_node = self._done[member] - if not class_node in node.locals.get(name, ()): + if class_node not in node.locals.get(name, ()): node.add_local_node(class_node, name) else: class_node = object_build_class(node, member, name) @@ -294,15 +302,15 @@ class InspectBuilder(object): self.object_build(class_node, member) if name == '__class__' and class_node.parent is None: class_node.parent = self._done[self._module] - elif ismethoddescriptor(member): + elif inspect.ismethoddescriptor(member): assert isinstance(member, object) object_build_methoddescriptor(node, member, name) - elif isdatadescriptor(member): + elif inspect.isdatadescriptor(member): assert isinstance(member, object) object_build_datadescriptor(node, member, name) elif isinstance(member, _CONSTANTS): attach_const_node(node, name, member) - elif isroutine(member): + elif inspect.isroutine(member): # This should be called for Jython, where some builtin # methods aren't catched by isbuiltin branch. _build_from_function(node, name, member, self._module) @@ -327,7 +335,7 @@ class InspectBuilder(object): # Python 2.5.1 (r251:54863, Sep 1 2010, 22:03:14) # >>> print object.__new__.__module__ # None - modname = BUILTINS + modname = six.moves.builtins.__name__ else: attach_dummy_node(node, name, member) return True @@ -359,10 +367,10 @@ def _astroid_bootstrapping(astroid_builtin=None): # this boot strapping is necessary since we need the Const nodes to # inspect_build builtins, and then we can proxy Const if astroid_builtin is None: - from logilab.common.compat import builtins + from six.moves import builtins astroid_builtin = Astroid_BUILDER.inspect_build(builtins) - for cls, node_cls in CONST_CLS.items(): + for cls, node_cls in node_classes.CONST_CLS.items(): if cls is type(None): proxy = build_class('NoneType') proxy.parent = astroid_builtin @@ -383,10 +391,9 @@ _astroid_bootstrapping() # infinite recursion (see https://bugs.launchpad.net/pylint/+bug/456870) def _set_proxied(const): return _CONST_PROXY[const.value.__class__] -Const._proxied = property(_set_proxied) +nodes.Const._proxied = property(_set_proxied) -from types import GeneratorType -_GeneratorType = ClassDef(GeneratorType.__name__, GeneratorType.__doc__) -_GeneratorType.parent = MANAGER.astroid_cache[BUILTINS] -Generator._proxied = _GeneratorType -Astroid_BUILDER.object_build(Generator._proxied, GeneratorType) +_GeneratorType = nodes.ClassDef(types.GeneratorType.__name__, types.GeneratorType.__doc__) +_GeneratorType.parent = MANAGER.astroid_cache[six.moves.builtins.__name__] +bases.Generator._proxied = _GeneratorType +Astroid_BUILDER.object_build(bases.Generator._proxied, types.GeneratorType) diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py index 8b2ef48f..6399ef01 100644 --- a/astroid/rebuilder.py +++ b/astroid/rebuilder.py @@ -18,62 +18,51 @@ """this module contains utilities for rebuilding a _ast tree in order to get a single Astroid representation """ -from astroid.as_string import dump +import _ast import sys -from _ast import ( - Expr, Str, - # binary operators - Add, BinOp, Div, FloorDiv, Mod, Mult, Pow, Sub, BitAnd, BitOr, BitXor, - LShift, RShift, - # logical operators - And, Or, - # unary operators - UAdd, USub, Not, Invert, - # comparison operators - Eq, Gt, GtE, In, Is, IsNot, Lt, LtE, NotEq, NotIn, - ) -from astroid import nodes from astroid import astpeephole +from astroid import nodes + + -_BIN_OP_CLASSES = {Add: '+', - BitAnd: '&', - BitOr: '|', - BitXor: '^', - Div: '/', - FloorDiv: '//', - Mod: '%', - Mult: '*', - Pow: '**', - Sub: '-', - LShift: '<<', - RShift: '>>', +_BIN_OP_CLASSES = {_ast.Add: '+', + _ast.BitAnd: '&', + _ast.BitOr: '|', + _ast.BitXor: '^', + _ast.Div: '/', + _ast.FloorDiv: '//', + _ast.Mod: '%', + _ast.Mult: '*', + _ast.Pow: '**', + _ast.Sub: '-', + _ast.LShift: '<<', + _ast.RShift: '>>', } if sys.version_info >= (3, 5): - from _ast import MatMult - _BIN_OP_CLASSES[MatMult] = '@' + _BIN_OP_CLASSES[_ast.MatMult] = '@' -_BOOL_OP_CLASSES = {And: 'and', - Or: 'or', +_BOOL_OP_CLASSES = {_ast.And: 'and', + _ast.Or: 'or', } -_UNARY_OP_CLASSES = {UAdd: '+', - USub: '-', - Not: 'not', - Invert: '~', +_UNARY_OP_CLASSES = {_ast.UAdd: '+', + _ast.USub: '-', + _ast.Not: 'not', + _ast.Invert: '~', } -_CMP_OP_CLASSES = {Eq: '==', - Gt: '>', - GtE: '>=', - In: 'in', - Is: 'is', - IsNot: 'is not', - Lt: '<', - LtE: '<=', - NotEq: '!=', - NotIn: 'not in', +_CMP_OP_CLASSES = {_ast.Eq: '==', + _ast.Gt: '>', + _ast.GtE: '>=', + _ast.In: 'in', + _ast.Is: 'is', + _ast.IsNot: 'is not', + _ast.Lt: '<', + _ast.LtE: '<=', + _ast.NotEq: '!=', + _ast.NotIn: 'not in', } CONST_NAME_TRANSFORMS = {'None': None, @@ -93,7 +82,7 @@ PY34 = sys.version_info >= (3, 4) def _get_doc(node): try: - if isinstance(node.body[0], Expr) and isinstance(node.body[0].value, Str): + if isinstance(node.body[0], _ast.Expr) and isinstance(node.body[0].value, _ast.Str): doc = node.body[0].value.s node.body = node.body[1:] return node, doc @@ -111,7 +100,6 @@ class TreeRebuilder(object): self._import_from_nodes = [] self._delayed_assattr = [] self._visit_meths = {} - self._transform = manager.transform self._peepholer = astpeephole.ASTPeepholeOptimizer() def visit_module(self, node, modname, modpath, package): @@ -131,7 +119,7 @@ class TreeRebuilder(object): visit_name = 'visit_' + REDIRECT.get(cls_name, cls_name).lower() visit_method = getattr(self, visit_name) self._visit_meths[cls] = visit_method - return self._transform(visit_method(node, parent, assign_ctx)) + return visit_method(node, parent, assign_ctx) def _save_assignment(self, node, name=None): """save assignement situation since node.parent is not available yet""" @@ -259,7 +247,7 @@ class TreeRebuilder(object): def visit_binop(self, node, parent, assign_ctx=None): """visit a BinOp node by returning a fresh instance of it""" - if isinstance(node.left, BinOp) and self._manager.optimize_ast: + if isinstance(node.left, _ast.BinOp) and self._manager.optimize_ast: # Optimize BinOp operations in order to remove # redundant recursion. For instance, if the # following code is parsed in order to obtain @@ -272,7 +260,7 @@ class TreeRebuilder(object): # problem for the correctness of the program). # # ("a" + "b" + # one thousand more + "c") - newnode = self._peepholer.optimize_binop(node, parent) + newnode = self._peepholer.optimize_binop(node) if newnode: return newnode diff --git a/astroid/scoped_nodes.py b/astroid/scoped_nodes.py index 8d053c8f..fbe58759 100644 --- a/astroid/scoped_nodes.py +++ b/astroid/scoped_nodes.py @@ -28,13 +28,15 @@ import sys import warnings import six -from logilab.common import decorators as decorators_mod from astroid import bases +from astroid import context as contextmod from astroid import exceptions from astroid import manager from astroid import mixins from astroid import node_classes +from astroid import decorators as decorators_mod +from astroid import util BUILTINS = six.moves.builtins.__name__ @@ -244,7 +246,6 @@ class LocalsDictNodeNG(node_classes.LookupMixIn, bases.NodeNG): def __contains__(self, name): return name in self.locals - has_key = __contains__ class Module(LocalsDictNodeNG): @@ -376,7 +377,7 @@ class Module(LocalsDictNodeNG): """inferred getattr""" # set lookup name since this is necessary to infer on import nodes for # instance - context = bases.copy_context(context) + context = contextmod.copy_context(context) context.lookupname = name try: return bases._infer_stmts(self.getattr(name, context), @@ -462,19 +463,6 @@ class Module(LocalsDictNodeNG): 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 astroid - # # We separate the different steps of lookup in try/excepts # to avoid catching too many Exceptions default = [name for name in self.keys() if not name.startswith('_')] @@ -566,7 +554,7 @@ class DictComp(ComprehensionScope): self.generators = generators def bool_value(self): - return bases.YES + return util.YES class SetComp(ComprehensionScope): @@ -587,7 +575,7 @@ class SetComp(ComprehensionScope): self.generators = generators def bool_value(self): - return bases.YES + return util.YES class _ListComp(bases.NodeNG): @@ -601,7 +589,7 @@ class _ListComp(bases.NodeNG): self.generators = generators def bool_value(self): - return bases.YES + return util.YES if six.PY3: @@ -641,50 +629,6 @@ def _infer_decorator_callchain(node): return 'staticmethod' -def _function_type(self): - """ - FunctionDef type, possible values are: - method, function, staticmethod, classmethod. - """ - # Can't infer that this node is decorated - # with a subclass of `classmethod` where `type` is first set, - # so do it here. - if self.decorators: - for node in self.decorators.nodes: - if isinstance(node, node_classes.Call): - # Handle the following case: - # @some_decorator(arg1, arg2) - # def func(...) - # - try: - current = next(node.func.infer()) - except exceptions.InferenceError: - continue - _type = _infer_decorator_callchain(current) - if _type is not None: - return _type - - try: - for inferred in node.infer(): - # Check to see if this returns a static or a class method. - _type = _infer_decorator_callchain(inferred) - if _type is not None: - return _type - - if not isinstance(inferred, ClassDef): - continue - for ancestor in inferred.ancestors(): - if not isinstance(ancestor, ClassDef): - continue - if ancestor.is_subtype_of('%s.classmethod' % BUILTINS): - return 'classmethod' - elif ancestor.is_subtype_of('%s.staticmethod' % BUILTINS): - return 'staticmethod' - except exceptions.InferenceError: - pass - return self._type - - class Lambda(LocalsDictNodeNG, mixins.FilterStmtsMixin): _astroid_fields = ('args', 'body',) _other_fields = ('locals',) @@ -759,16 +703,13 @@ class FunctionDef(bases.Statement, Lambda): special_attributes = set(('__name__', '__doc__', '__dict__')) is_function = True # attributes below are set by the builder module or by raw factories - type = decorators_mod.cachedproperty(_function_type) - - _type = "function" + decorators = None _other_fields = ('locals', 'name', 'doc', '_type', 'decorators') def __init__(self, name=None, doc=None, lineno=None, col_offset=None, parent=None): self.name = name self.doc = doc - self.extra_decorators = [] self.instance_attrs = {} super(FunctionDef, self).__init__(lineno, col_offset, parent) if parent: @@ -794,6 +735,100 @@ class FunctionDef(bases.Statement, Lambda): self.returns = returns @decorators_mod.cachedproperty + def extra_decorators(self): + """Get the extra decorators that this function can have + + Additional decorators are considered when they are used as + assignments, as in `method = staticmethod(method)`. + The property will return all the callables that are used for + decoration. + """ + frame = self.parent.frame() + if not isinstance(frame, Class): + return [] + + decorators = [] + for assign in frame.nodes_of_class(node_classes.Assign): + if (isinstance(assign.value, node_classes.CallFunc) + and isinstance(assign.value.func, node_classes.Name)): + for assign_node in assign.targets: + if not isinstance(assign_node, node_classes.AssName): + # Support only `name = callable(name)` + continue + + if assign_node.name != self.name: + # Interested only in the assignment nodes that + # decorates the current method. + continue + try: + meth = frame[self.name] + except KeyError: + continue + else: + if isinstance(meth, Function): + decorators.append(assign.value) + return decorators + + @decorators_mod.cachedproperty + def type(self): + """Get the function type for this node. + + Possible values are: method, function, staticmethod, classmethod. + """ + builtin_descriptors = {'classmethod', 'staticmethod'} + + for decorator in self.extra_decorators: + if decorator.func.name in builtin_descriptors: + return decorator.func.name + + frame = self.parent.frame() + type_name = 'function' + if isinstance(frame, Class): + if self.name == '__new__': + return'classmethod' + else: + type_name = 'method' + + if self.decorators: + for node in self.decorators.nodes: + if isinstance(node, node_classes.Name): + if node.name in builtin_descriptors: + return node.name + + if isinstance(node, node_classes.CallFunc): + # Handle the following case: + # @some_decorator(arg1, arg2) + # def func(...) + # + try: + current = next(node.func.infer()) + except exceptions.InferenceError: + continue + _type = _infer_decorator_callchain(current) + if _type is not None: + return _type + + try: + for infered in node.infer(): + # Check to see if this returns a static or a class method. + _type = _infer_decorator_callchain(infered) + if _type is not None: + return _type + + if not isinstance(infered, Class): + continue + for ancestor in infered.ancestors(): + if not isinstance(ancestor, Class): + continue + if ancestor.is_subtype_of('%s.classmethod' % BUILTINS): + return 'classmethod' + elif ancestor.is_subtype_of('%s.staticmethod' % BUILTINS): + return 'staticmethod' + except exceptions.InferenceError: + pass + return type_name + + @decorators_mod.cachedproperty def fromlineno(self): # lineno is the line number of the first decorator, we want the def # statement lineno @@ -912,7 +947,7 @@ class FunctionDef(bases.Statement, Lambda): c.hide = True c.parent = self class_bases = [next(b.infer(context)) for b in caller.args[1:]] - c.bases = [base for base in class_bases if base != bases.YES] + c.bases = [base for base in class_bases if base != util.YES] c._metaclass = metaclass yield c return @@ -925,7 +960,7 @@ class FunctionDef(bases.Statement, Lambda): for inferred in returnnode.value.infer(context): yield inferred except exceptions.InferenceError: - yield bases.YES + yield util.YES def bool_value(self): return True @@ -962,7 +997,7 @@ def _is_metaclass(klass, seen=None): if isinstance(baseobj, bases.Instance): # not abstract return False - if baseobj is bases.YES: + if baseobj is util.YES: continue if baseobj is klass: continue @@ -1011,7 +1046,24 @@ def _class_type(klass, ancestors=None): return klass._type -class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): +def get_wrapping_class(node): + """Obtain the class that *wraps* this node + + We consider that a class wraps a node if the class + is a parent for the said node. + """ + + klass = node.frame() + while klass is not None and not isinstance(klass, Class): + if klass.parent is None: + klass = None + else: + klass = klass.parent.frame() + return klass + + + +class Class(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): # some of the attributes below are set by the builder module or # by a raw factories @@ -1054,14 +1106,14 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): def _newstyle_impl(self, context=None): if context is None: - context = bases.InferenceContext() + context = contextmod.InferenceContext() if self._newstyle is not None: return self._newstyle for base in self.ancestors(recurs=False, context=context): if base._newstyle_impl(context): self._newstyle = True break - klass = self._explicit_metaclass() + klass = self.declared_metaclass() # could be any callable, we'd need to infer the result of klass(name, # bases, dict). punt if it's not a class node. if klass is not None and isinstance(klass, ClassDef): @@ -1113,7 +1165,7 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): isinstance(name_node.value, six.string_types)): name = name_node.value else: - return bases.YES + return util.YES result = ClassDef(name, None) @@ -1125,7 +1177,7 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): # There is currently no AST node that can represent an 'unknown' # node (YES is not an AST node), therefore we simply return YES here # although we know at least the name of the class. - return bases.YES + return util.YES # Get the members of the class try: @@ -1192,7 +1244,7 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): # FIXME: inference make infinite loops possible here yielded = set([self]) if context is None: - context = bases.InferenceContext() + context = contextmod.InferenceContext() if six.PY3: if not self.bases and self.qname() != 'builtins.object': yield builtin_lookup("object")[1][0] @@ -1294,12 +1346,18 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): """return Instance of ClassDef node, else return self""" return bases.Instance(self) - def getattr(self, name, context=None): - """this method doesn't look in the instance_attrs dictionary since it's - done by an Instance proxy at inference time. + def getattr(self, name, context=None, class_context=True): + """Get an attribute from this class, using Python's attribute semantic + 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 + found but a __getattr__ or __getattribute__ method is defined. + If *class_context* is given, then it's considered that the attribute + is accessed from a class context, e.g. Class.attribute, otherwise + it might have been accessed from an instance as well. + If *class_context* is used in that case, then a lookup in the + implicit metaclass and the explicit metaclass will be done. """ values = self.locals.get(name, []) if name in self.special_attributes: @@ -1318,17 +1376,62 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): values = list(values) for classnode in self.ancestors(recurs=True, context=context): values += classnode.locals.get(name, []) + + if class_context: + values += self._metaclass_lookup_attribute(name, context) if not values: raise exceptions.NotFoundError(name) return values + def _metaclass_lookup_attribute(self, name, context): + """Search the given name in the implicit and the explicit metaclass.""" + attrs = set() + implicit_meta = self.implicit_metaclass() + metaclass = self.metaclass() + for cls in {implicit_meta, metaclass}: + if cls and cls != self: + cls_attributes = self._get_attribute_from_metaclass( + cls, name, context) + attrs.update(set(cls_attributes)) + return attrs + + def _get_attribute_from_metaclass(self, cls, name, context): + try: + attrs = cls.getattr(name, context=context, + class_context=True) + except exceptions.NotFoundError: + return + + for attr in bases._infer_stmts(attrs, context, frame=cls): + if not isinstance(attr, Function): + yield attr + continue + + if bases._is_property(attr): + # TODO(cpopa): don't use a private API. + for infered in attr.infer_call_result(self, context): + yield infered + continue + if attr.type == 'classmethod': + # If the method is a classmethod, then it will + # be bound to the metaclass, not to the class + # from where the attribute is retrieved. + # get_wrapping_class could return None, so just + # default to the current class. + frame = get_wrapping_class(attr) or self + yield bases.BoundMethod(attr, frame) + elif attr.type == 'staticmethod': + yield attr + else: + yield bases.BoundMethod(attr, self) + def igetattr(self, name, context=None): """inferred getattr, need special treatment in class to handle descriptors """ # set lookup name since this is necessary to infer on import nodes for # instance - context = bases.copy_context(context) + context = contextmod.copy_context(context) context.lookupname = name try: for inferred in bases._infer_stmts(self.getattr(name, context), @@ -1341,13 +1444,13 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): except exceptions.NotFoundError: yield inferred else: - yield bases.YES + yield util.YES else: yield function_to_method(inferred, self) except exceptions.NotFoundError: if not name.startswith('__') and self.has_dynamic_getattr(context): # class handle some dynamic attributes, return a YES object - yield bases.YES + yield util.YES else: raise exceptions.InferenceError(name) @@ -1405,11 +1508,10 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): return builtin_lookup('type')[1][0] _metaclass = None - def _explicit_metaclass(self): - """ Return the explicit defined metaclass - for the current class. + def declared_metaclass(self): + """Return the explicit declared metaclass for the current class. - An explicit defined metaclass is defined + An explicit declared metaclass is defined either by passing the ``metaclass`` keyword argument in the class definition line (Python 3) or (Python 2) by having a ``__metaclass__`` class attribute, or if there are @@ -1429,7 +1531,7 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): # Expects this from Py3k TreeRebuilder try: return next(node for node in self._metaclass.infer() - if node is not bases.YES) + if node is not util.YES) except (exceptions.InferenceError, StopIteration): return None if six.PY3: @@ -1452,18 +1554,24 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): inferred = next(assignment.infer()) except exceptions.InferenceError: return +<<<<<<< variant A if inferred is bases.YES: # don't expose this +>>>>>>> variant B + if infered is util.YES: # don't expose this +####### Ancestor + if infered is bases.YES: # don't expose this +======= end return None return inferred def metaclass(self): - """ Return the metaclass of this class. + """Return the metaclass of this class. If this class does not define explicitly a metaclass, then the first defined metaclass in ancestors will be used instead. """ - klass = self._explicit_metaclass() + klass = self.declared_metaclass() if klass is None: for parent in self.ancestors(): klass = parent.metaclass() @@ -1503,7 +1611,7 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): values = [item[0] for item in slots.items] else: values = slots.itered() - if values is bases.YES: + if values is util.YES: continue if not values: # Stop the iteration, because the class @@ -1513,7 +1621,7 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): for elt in values: try: for inferred in elt.infer(): - if inferred is bases.YES: + if inferred is util.YES: continue if (not isinstance(inferred, node_classes.Const) or not isinstance(inferred.value, @@ -1566,7 +1674,7 @@ class ClassDef(bases.Statement, LocalsDictNodeNG, mixins.FilterStmtsMixin): # only in SomeClass. if context is None: - context = bases.InferenceContext() + context = contextmod.InferenceContext() if six.PY3: if not self.bases and self.qname() != 'builtins.object': yield builtin_lookup("object")[1][0] diff --git a/astroid/tests/testdata/python2/data/module.py b/astroid/tests/testdata/python2/data/module.py index dc206e22..6a67b9b6 100644 --- a/astroid/tests/testdata/python2/data/module.py +++ b/astroid/tests/testdata/python2/data/module.py @@ -2,7 +2,7 @@ """ __revision__ = '$Id: module.py,v 1.2 2005-11-02 11:56:54 syt Exp $' -from logilab.common.shellutils import ProgressBar as pb +from astroid.node_classes import Name as NameNode from astroid import modutils from astroid.utils import * import os.path diff --git a/astroid/tests/testdata/python2/data/notamodule/file.py b/astroid/tests/testdata/python2/data/notamodule/file.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/astroid/tests/testdata/python2/data/notamodule/file.py diff --git a/astroid/tests/testdata/python3/data/module.py b/astroid/tests/testdata/python3/data/module.py index ec5b64aa..6b47d9d5 100644 --- a/astroid/tests/testdata/python3/data/module.py +++ b/astroid/tests/testdata/python3/data/module.py @@ -2,7 +2,7 @@ """ __revision__ = '$Id: module.py,v 1.2 2005-11-02 11:56:54 syt Exp $' -from logilab.common.shellutils import ProgressBar as pb +from astroid.node_classes import Name as NameNode from astroid import modutils from astroid.utils import * import os.path diff --git a/astroid/tests/testdata/python3/data/notamodule/file.py b/astroid/tests/testdata/python3/data/notamodule/file.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/astroid/tests/testdata/python3/data/notamodule/file.py diff --git a/astroid/tests/unittest_brain.py b/astroid/tests/unittest_brain.py index ddae51f9..06883a2f 100644 --- a/astroid/tests/unittest_brain.py +++ b/astroid/tests/unittest_brain.py @@ -25,6 +25,7 @@ from astroid import bases from astroid import builder from astroid import nodes from astroid import test_utils +from astroid import util import astroid @@ -105,7 +106,7 @@ class NamedTupleTest(unittest.TestCase): def foo(fields): return __(namedtuple("foo", fields)) """) - self.assertIs(bases.YES, next(klass.infer())) + self.assertIs(util.YES, next(klass.infer())) @unittest.skipIf(sys.version_info[0] > 2, 'namedtuple inference is broken on Python 3') @@ -137,7 +138,8 @@ class NamedTupleTest(unittest.TestCase): class ModuleExtenderTest(unittest.TestCase): def testExtensionModules(self): - for extender, _ in MANAGER.transforms[nodes.Module]: + transformer = MANAGER._transform + for extender, _ in transformer.transforms[nodes.Module]: n = nodes.Module('__main__', None) extender(n) diff --git a/astroid/tests/unittest_builder.py b/astroid/tests/unittest_builder.py index 252f096f..84bf50c2 100644 --- a/astroid/tests/unittest_builder.py +++ b/astroid/tests/unittest_builder.py @@ -23,12 +23,12 @@ import unittest import six -from astroid import bases from astroid import builder from astroid import exceptions from astroid import manager from astroid import nodes from astroid import test_utils +from astroid import util from astroid.tests import resources MANAGER = manager.AstroidManager() @@ -482,7 +482,7 @@ class BuilderTest(unittest.TestCase): n = test_utils.get_name_node(astroid, 'n') self.assertIsNot(n.scope(), astroid) self.assertEqual([i.__class__ for i in n.infer()], - [bases.YES.__class__]) + [util.YES.__class__]) def test_no_future_imports(self): mod = builder.parse("import sys") @@ -567,19 +567,6 @@ class BuilderTest(unittest.TestCase): self.assertIsInstance(chain, nodes.Const) self.assertEqual(chain.value, 'None') - def test_lgc_classproperty(self): - '''test expected values of constants after rebuilding''' - code = ''' - from logilab.common.decorators import classproperty - - class A(object): - @classproperty - def hop(cls): #@ - return None - ''' - method = test_utils.extract_node(code) - self.assertEqual('classmethod', method.type) - def test_not_implemented(self): node = test_utils.extract_node(''' NotImplemented #@ @@ -615,9 +602,9 @@ class FileBuildTest(unittest.TestCase): _locals = module.locals self.assertIs(_locals, module.globals) keys = sorted(_locals.keys()) - should = ['MY_DICT', 'YO', 'YOUPI', + should = ['MY_DICT', 'NameNode', 'YO', 'YOUPI', '__revision__', 'global_access', 'modutils', 'four_args', - 'os', 'redirect', 'pb'] + 'os', 'redirect'] should.sort() self.assertEqual(keys, sorted(should)) diff --git a/astroid/tests/unittest_helpers.py b/astroid/tests/unittest_helpers.py index a69c1361..3065250c 100644 --- a/astroid/tests/unittest_helpers.py +++ b/astroid/tests/unittest_helpers.py @@ -21,12 +21,12 @@ import unittest import six
from six.moves import builtins
-from astroid import bases
from astroid import builder
from astroid import helpers
from astroid import manager
from astroid import raw_building
from astroid import test_utils
+from astroid import util
class TestHelpers(unittest.TestCase):
@@ -154,7 +154,7 @@ class TestHelpers(unittest.TestCase): from unknown import Unknown
u = Unknown #@
''')
- self.assertEqual(helpers.object_type(node), bases.YES)
+ self.assertEqual(helpers.object_type(node), util.YES)
def test_object_type_too_many_types(self):
node = test_utils.extract_node('''
@@ -166,7 +166,7 @@ class TestHelpers(unittest.TestCase): return 1
test(Unknown) #@
''')
- self.assertEqual(helpers.object_type(node), bases.YES)
+ self.assertEqual(helpers.object_type(node), util.YES)
def test_is_subtype(self):
ast_nodes = test_utils.extract_node('''
@@ -216,8 +216,8 @@ class TestHelpers(unittest.TestCase): class F(D, E): pass #@
''')
self.assertFalse(helpers.is_subtype(cls_e, cls_f))
- self.assertEqual(helpers.is_subtype(cls_f, cls_e), bases.YES)
- self.assertEqual(helpers.is_supertype(cls_e, cls_f), bases.YES)
+ self.assertEqual(helpers.is_subtype(cls_f, cls_e), util.YES)
+ self.assertEqual(helpers.is_supertype(cls_e, cls_f), util.YES)
self.assertFalse(helpers.is_supertype(cls_f, cls_e))
def test_is_subtype_supertype_unknown_bases(self):
diff --git a/astroid/tests/unittest_inference.py b/astroid/tests/unittest_inference.py index 4674e92b..5fb85430 100644 --- a/astroid/tests/unittest_inference.py +++ b/astroid/tests/unittest_inference.py @@ -27,10 +27,11 @@ import six from astroid import InferenceError, builder, nodes from astroid.builder import parse from astroid.inference import infer_end as inference_infer_end -from astroid.bases import YES, Instance, BoundMethod, UnboundMethod,\ +from astroid.bases import Instance, BoundMethod, UnboundMethod,\ path_wrapper, BUILTINS from astroid import objects from astroid import test_utils +from astroid import util from astroid.tests import resources @@ -307,7 +308,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): self.assertIsInstance(obj1, nodes.Const) self.assertEqual(obj1.value, 0) obj1 = next(inferred) - self.assertIs(obj1, YES, obj1) + self.assertIs(obj1, util.YES, obj1) self.assertRaises(StopIteration, partial(next, inferred)) def test_args_default_inference2(self): @@ -316,13 +317,13 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): self.assertIsInstance(obj1, nodes.Const) self.assertEqual(obj1.value, 4) obj1 = next(inferred) - self.assertIs(obj1, YES, obj1) + self.assertIs(obj1, util.YES, obj1) self.assertRaises(StopIteration, partial(next, inferred)) def test_inference_restrictions(self): inferred = test_utils.get_name_node(self.ast['C']['meth1'], 'arg1').infer() obj1 = next(inferred) - self.assertIs(obj1, YES, obj1) + self.assertIs(obj1, util.YES, obj1) self.assertRaises(StopIteration, partial(next, inferred)) def test_ancestors_inference(self): @@ -544,7 +545,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): ast = parse(code, __name__) xxx = ast['xxx'] self.assertSetEqual({n.__class__ for n in xxx.inferred()}, - {nodes.Const, YES.__class__}) + {nodes.Const, util.YES.__class__}) def test_method_argument(self): code = ''' @@ -560,13 +561,13 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): ast = parse(code, __name__) arg = test_utils.get_name_node(ast['ErudiEntitySchema']['__init__'], 'e_type') self.assertEqual([n.__class__ for n in arg.infer()], - [YES.__class__]) + [util.YES.__class__]) arg = test_utils.get_name_node(ast['ErudiEntitySchema']['__init__'], 'kwargs') self.assertEqual([n.__class__ for n in arg.infer()], [nodes.Dict]) arg = test_utils.get_name_node(ast['ErudiEntitySchema']['meth'], 'e_type') self.assertEqual([n.__class__ for n in arg.infer()], - [YES.__class__]) + [util.YES.__class__]) arg = test_utils.get_name_node(ast['ErudiEntitySchema']['meth'], 'args') self.assertEqual([n.__class__ for n in arg.infer()], [nodes.Tuple]) @@ -704,7 +705,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): for node in ast_nodes[:3]: self.assertRaises(InferenceError, next, node.infer()) for node in ast_nodes[3:]: - self.assertEqual(next(node.infer()), YES) + self.assertEqual(next(node.infer()), util.YES) def test_bytes_subscript(self): node = test_utils.extract_node('''b'a'[0]''') @@ -947,7 +948,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): self.assertEqual(first.value, 43) second = next(ast_nodes[1].infer()) - self.assertEqual(second, YES) + self.assertEqual(second, util.YES) def test_binary_op_other_type_using_reflected_operands(self): ast_nodes = test_utils.extract_node(''' @@ -958,7 +959,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): 1 + A() #@ ''') first = next(ast_nodes[0].infer()) - self.assertEqual(first, YES) + self.assertEqual(first, util.YES) second = next(ast_nodes[1].infer()) self.assertIsInstance(second, nodes.Const) @@ -972,7 +973,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): 1 + A() #@ ''') first = next(ast_node.infer()) - self.assertEqual(first, YES) + self.assertEqual(first, util.YES) def test_binary_op_list_mul(self): for code in ('a = [[]] * 2', 'a = 2 * [[]]'): @@ -987,12 +988,28 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): def test_binary_op_list_mul_none(self): 'test correct handling on list multiplied by None' ast = builder.string_build('a = [1] * None\nb = [1] * "r"') +<<<<<<< variant A inferred = ast['a'].inferred() self.assertEqual(len(inferred), 1) self.assertEqual(inferred[0], YES) inferred = ast['b'].inferred() self.assertEqual(len(inferred), 1) self.assertEqual(inferred[0], YES) +>>>>>>> variant B + infered = ast['a'].infered() + self.assertEqual(len(infered), 1) + self.assertEqual(infered[0], util.YES) + infered = ast['b'].infered() + self.assertEqual(len(infered), 1) + self.assertEqual(infered[0], util.YES) +####### Ancestor + infered = ast['a'].infered() + self.assertEqual(len(infered), 1) + self.assertEqual(infered[0], YES) + infered = ast['b'].infered() + self.assertEqual(len(infered), 1) + self.assertEqual(infered[0], YES) +======= end def test_binary_op_tuple_add(self): ast = builder.string_build('a = (1,) + (2,)', __name__, __file__) @@ -1039,7 +1056,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): callfuncnode = test_utils.extract_node(code) inferred = list(callfuncnode.infer()) self.assertEqual(len(inferred), 2, inferred) - inferred.remove(YES) + inferred.remove(util.YES) self.assertIsInstance(inferred[0], nodes.Const) self.assertIsNone(inferred[0].value) @@ -1051,7 +1068,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): ast = parse(code, __name__) inferred = list(ast['f'].ilookup('a')) self.assertEqual(len(inferred), 1) - self.assertEqual(inferred[0], YES) + self.assertEqual(inferred[0], util.YES) def test_nonregr_instance_attrs(self): """non regression for instance_attrs infinite loop : pylint / #4""" @@ -1103,7 +1120,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): self.assertTrue(ast.absolute_import_activated(), True) inferred = next(test_utils.get_name_node(ast, 'import_package_subpackage_module').infer()) # failed to import since absolute_import is activated - self.assertIs(inferred, YES) + self.assertIs(inferred, util.YES) def test_nonregr_absolute_import(self): ast = resources.build_file('data/absimp/string.py', 'data.absimp.string') @@ -1221,7 +1238,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): ast = parse(code, __name__) inferred = list(test_utils.get_name_node(ast['foo'], 'spam').infer()) self.assertEqual(len(inferred), 1) - self.assertIs(inferred[0], YES) + self.assertIs(inferred[0], util.YES) def test_nonregr_func_global(self): code = ''' @@ -1291,6 +1308,14 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): inferred = list(n.igetattr('arg')) self.assertEqual(len(inferred), 1, inferred) + def test__new__bound_methods(self): + node = test_utils.extract_node(''' + class cls(object): pass + cls().__new__(cls) #@ + ''') + inferred = next(node.infer()) + self.assertIsInstance(inferred, Instance) + self.assertEqual(inferred._proxied, node.root()['cls']) def test_two_parents_from_same_module(self): code = ''' @@ -1404,7 +1429,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): ast = parse(code, __name__) sub = ast['sub'].inferred()[0] mul = ast['mul'].inferred()[0] - self.assertIs(sub, YES) + self.assertIs(sub, util.YES) self.assertIsInstance(mul, nodes.Const) self.assertEqual(mul.value, 42) @@ -1423,7 +1448,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): ast = parse(code, __name__) sub = ast['sub'].inferred()[0] mul = ast['mul'].inferred()[0] - self.assertIs(sub, YES) + self.assertIs(sub, util. YES) self.assertIsInstance(mul, nodes.Const) self.assertEqual(mul.value, 42) @@ -1443,7 +1468,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): ast = parse(code, __name__) sub = ast['sub'].inferred()[0] mul = ast['mul'].inferred()[0] - self.assertIs(sub, YES) + self.assertIs(sub, util.YES) self.assertIsInstance(mul, nodes.List) self.assertIsInstance(mul.elts[0], nodes.Const) self.assertEqual(mul.elts[0].value, 42) @@ -1460,12 +1485,12 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): """ ast = parse(code, __name__) node = ast['c'] - self.assertEqual(node.inferred(), [YES]) + self.assertEqual(node.inferred(), [util.YES]) def test_infer_empty_nodes(self): # Should not crash when trying to infer EmptyNodes. node = nodes.EmptyNode() - self.assertEqual(node.inferred(), [YES]) + self.assertEqual(node.inferred(), [util.YES]) def test_infinite_loop_for_decorators(self): # Issue https://bitbucket.org/logilab/astroid/issue/50 @@ -1933,7 +1958,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): def test_unary_op_leaks_stop_iteration(self): node = test_utils.extract_node('+[] #@') - self.assertEqual(YES, next(node.infer())) + self.assertEqual(util.YES, next(node.infer())) def test_unary_operands(self): ast_nodes = test_utils.extract_node(''' @@ -1984,7 +2009,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): for bad_node in ast_nodes[4:]: inferred = next(bad_node.infer()) - self.assertEqual(inferred, YES) + self.assertEqual(inferred, util.YES) def test_binary_op_type_errors(self): ast_nodes = test_utils.extract_node(''' @@ -2173,11 +2198,11 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): genexpr = next(module['genexpr'].infer()) self.assertTrue(genexpr.bool_value()) dict_comp = next(module['dict_comp'].infer()) - self.assertEqual(dict_comp, YES) + self.assertEqual(dict_comp, util.YES) set_comp = next(module['set_comp'].infer()) - self.assertEqual(set_comp, YES) + self.assertEqual(set_comp, util.YES) list_comp = next(module['list_comp'].infer()) - self.assertEqual(list_comp, YES) + self.assertEqual(list_comp, util.YES) lambda_func = next(module['lambda_func'].infer()) self.assertTrue(lambda_func) unbound_method = next(module['unbound_method'].infer()) @@ -2191,13 +2216,13 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): bin_op = module['bin_op'].parent.value self.assertTrue(bin_op.bool_value()) bool_op = module['bool_op'].parent.value - self.assertEqual(bool_op.bool_value(), YES) + self.assertEqual(bool_op.bool_value(), util.YES) callfunc = module['callfunc'].parent.value - self.assertEqual(callfunc.bool_value(), YES) + self.assertEqual(callfunc.bool_value(), util.YES) good_callfunc = next(module['good_callfunc'].infer()) self.assertTrue(good_callfunc.bool_value()) compare = module['compare'].parent.value - self.assertEqual(compare.bool_value(), YES) + self.assertEqual(compare.bool_value(), util.YES) def test_bool_value_instances(self): instances = test_utils.extract_node(''' @@ -2230,7 +2255,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): AlwaysTrueInstance() #@ ErrorInstance() #@ '''.format(bool=BOOL_SPECIAL_METHOD)) - expected = (False, True, False, True, True, YES, YES) + expected = (False, True, False, True, True, util.YES, util.YES) for node, expected_value in zip(instances, expected): inferred = next(node.infer()) self.assertEqual(inferred.bool_value(), expected_value) @@ -2302,7 +2327,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): A() + B() #@ ''') inferred = next(node.infer()) - self.assertEqual(inferred, YES) + self.assertEqual(inferred, util.YES) def test_binop_different_types_reflected_and_normal_not_implemented(self): node = test_utils.extract_node(''' @@ -2313,7 +2338,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): A() + B() #@ ''') inferred = next(node.infer()) - self.assertEqual(inferred, YES) + self.assertEqual(inferred, util.YES) def test_binop_subtype(self): node = test_utils.extract_node(''' @@ -2346,7 +2371,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): B() + A() #@ ''') inferred = next(node.infer()) - self.assertEqual(inferred, YES) + self.assertEqual(inferred, util.YES) def test_binop_supertype(self): node = test_utils.extract_node(''' @@ -2385,7 +2410,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): A() + B() #@ ''') inferred = next(node.infer()) - self.assertEqual(inferred, YES) + self.assertEqual(inferred, util.YES) def test_binop_inferrence_errors(self): ast_nodes = test_utils.extract_node(''' @@ -2400,7 +2425,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): A() + B() #@ ''') for node in ast_nodes: - self.assertEqual(next(node.infer()), YES) + self.assertEqual(next(node.infer()), util.YES) def test_binop_ambiguity(self): ast_nodes = test_utils.extract_node(''' @@ -2423,7 +2448,27 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): C() + A() #@ ''') for node in ast_nodes: - self.assertEqual(next(node.infer()), YES) + self.assertEqual(next(node.infer()), util.YES) + + def test_bin_op_supertype_more_complicated_example(self): + ast_node = test_utils.extract_node(''' + class A(object): + def __init__(self): + self.foo = 42 + def __add__(self, other): + return other.bar + self.foo / 2 + + class B(A): + def __init__(self): + self.bar = 24 + def __radd__(self, other): + return NotImplemented + + A() + B() #@ + ''') + inferred = next(ast_node.infer()) + self.assertIsInstance(inferred, nodes.Const) + self.assertEqual(int(inferred.value), 45) def test_aug_op_same_type_not_implemented(self): ast_node = test_utils.extract_node(''' @@ -2432,7 +2477,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): def __add__(self, other): return NotImplemented A() + A() #@ ''') - self.assertEqual(next(ast_node.infer()), YES) + self.assertEqual(next(ast_node.infer()), util.YES) def test_aug_op_same_type_aug_implemented(self): ast_node = test_utils.extract_node(''' @@ -2467,7 +2512,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): b = B() b+=A() #@ ''') - self.assertEqual(next(ast_node.infer()), YES) + self.assertEqual(next(ast_node.infer()), util.YES) def test_aug_op_subtype_aug_op_is_implemented(self): ast_node = test_utils.extract_node(''' @@ -2502,7 +2547,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): f = A() f += B() #@ ''') - self.assertEqual(next(ast_node.infer()), YES) + self.assertEqual(next(ast_node.infer()), util.YES) def test_aug_different_types_augop_implemented(self): ast_node = test_utils.extract_node(''' @@ -2550,7 +2595,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): a = A() a += B() #@ ''') - self.assertEqual(next(ast_node.infer()), YES) + self.assertEqual(next(ast_node.infer()), util.YES) def test_augop_supertypes_not_implemented_returned_for_all(self): ast_node = test_utils.extract_node(''' @@ -2562,7 +2607,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): a = A() a += B() #@ ''') - self.assertEqual(next(ast_node.infer()), YES) + self.assertEqual(next(ast_node.infer()), util.YES) def test_augop_supertypes_augop_implemented(self): ast_node = test_utils.extract_node(''' @@ -2634,7 +2679,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): [1, 2, 1, 2]) for rest in ast_nodes[1:]: inferred = next(rest.infer()) - self.assertEqual(inferred, YES) + self.assertEqual(inferred, util.YES) def test_special_method_masquerading_as_another(self): ast_node = test_utils.extract_node(''' @@ -2666,7 +2711,7 @@ class InferenceTest(resources.SysPathSetup, unittest.TestCase): class GetattrTest(unittest.TestCase): - def test_yes(self): + def test_yes_when_unknown(self): ast_nodes = test_utils.extract_node(''' from missing import Missing getattr(1, Unknown) #@ @@ -2684,7 +2729,7 @@ class GetattrTest(unittest.TestCase): for node in ast_nodes[4:]: inferred = next(node.infer()) - self.assertEqual(inferred, YES, node) + self.assertEqual(inferred, util.YES, node) def test_attrname_not_string(self): ast_nodes = test_utils.extract_node(''' @@ -2781,7 +2826,7 @@ class HasattrTest(unittest.TestCase): ''') for node in ast_nodes: inferred = next(node.infer()) - self.assertEqual(inferred, YES) + self.assertEqual(inferred, util.YES) def test_attribute_is_missing(self): ast_nodes = test_utils.extract_node(''' @@ -2854,7 +2899,7 @@ class BoolOpTest(unittest.TestCase): ''') for node in ast_nodes: inferred = next(node.infer()) - self.assertEqual(inferred, YES) + self.assertEqual(inferred, util.YES) def test_other_nodes(self): ast_nodes = test_utils.extract_node(''' @@ -2934,7 +2979,7 @@ class TestCallable(unittest.TestCase): ''') for node in ast_nodes: inferred = next(node.infer()) - self.assertEqual(inferred, YES) + self.assertEqual(inferred, util.YES) def test_not_callable(self): ast_nodes = test_utils.extract_node(''' @@ -2960,12 +3005,12 @@ class TestBool(unittest.TestCase): ('bool(True)', True), ('bool(False)', False), ('bool(None)', False), - ('from unknown import Unknown; __(bool(Unknown))', YES), + ('from unknown import Unknown; __(bool(Unknown))', util.YES), ] for code, expected in pairs: node = test_utils.extract_node(code) inferred = next(node.infer()) - if expected is YES: + if expected is util.YES: self.assertEqual(expected, inferred) else: self.assertEqual(inferred.value, expected) diff --git a/astroid/tests/unittest_lookup.py b/astroid/tests/unittest_lookup.py index 28c60209..805efd9e 100644 --- a/astroid/tests/unittest_lookup.py +++ b/astroid/tests/unittest_lookup.py @@ -21,12 +21,12 @@ import functools import sys import unittest -from astroid import bases from astroid import builder from astroid import exceptions from astroid import nodes from astroid import scoped_nodes from astroid import test_utils +from astroid import util from astroid.tests import resources @@ -172,7 +172,7 @@ class LookupTest(resources.SysPathSetup, unittest.TestCase): """) var = astroid.body[1].value if sys.version_info < (3, 0): - self.assertEqual(var.inferred(), [bases.YES]) + self.assertEqual(var.inferred(), [util.YES]) else: self.assertRaises(exceptions.UnresolvableName, var.inferred) @@ -221,8 +221,8 @@ class LookupTest(resources.SysPathSetup, unittest.TestCase): iterer = count() num = iterer.next() """) - next = tree.body[2].value.func - gener = next.expr.inferred()[0] + next_node = tree.body[2].value.func + gener = next_node.expr.inferred()[0] if sys.version_info < (3, 0): self.assertIsInstance(gener.getattr('next')[0], nodes.FunctionDef) else: diff --git a/astroid/tests/unittest_modutils.py b/astroid/tests/unittest_modutils.py index 95cb1f74..644d33f5 100644 --- a/astroid/tests/unittest_modutils.py +++ b/astroid/tests/unittest_modutils.py @@ -22,7 +22,6 @@ import os import sys import unittest -from logilab.common import configuration from astroid import modutils from astroid.tests import resources @@ -99,8 +98,9 @@ class ModPathFromFileTest(unittest.TestCase): """ given an absolute file path return the python module's path as a list """ def test_knownValues_modpath_from_file_1(self): - self.assertEqual(modutils.modpath_from_file(configuration.__file__), - ['logilab', 'common', 'configuration']) + from xml.etree import ElementTree + self.assertEqual(modutils.modpath_from_file(ElementTree.__file__), + ['xml', 'etree', 'ElementTree']) def test_knownValues_modpath_from_file_2(self): self.assertEqual(modutils.modpath_from_file('unittest_modutils.py', @@ -189,7 +189,7 @@ class StandardLibModuleTest(resources.SysPathSetup, unittest.TestCase): self.assertEqual(modutils.is_standard_module('marshal'), True) def test_nonstandard(self): - self.assertEqual(modutils.is_standard_module('logilab'), False) + self.assertEqual(modutils.is_standard_module('astroid'), False) def test_unknown(self): self.assertEqual(modutils.is_standard_module('unknown'), False) @@ -210,14 +210,14 @@ class StandardLibModuleTest(resources.SysPathSetup, unittest.TestCase): self.assertEqual(modutils.is_standard_module('data.module', (os.path.abspath(datadir),)), True) def test_failing_edge_cases(self): - from logilab import common + from xml import etree # using a subpackage/submodule path as std_path argument - self.assertEqual(modutils.is_standard_module('logilab.common', common.__path__), False) + self.assertEqual(modutils.is_standard_module('xml.etree', etree.__path__), False) # using a module + object name as modname argument self.assertEqual(modutils.is_standard_module('sys.path'), True) # this is because only the first package/module is considered self.assertEqual(modutils.is_standard_module('sys.whatever'), True) - self.assertEqual(modutils.is_standard_module('logilab.whatever', common.__path__), False) + self.assertEqual(modutils.is_standard_module('xml.whatever', etree.__path__), False) class IsRelativeTest(unittest.TestCase): @@ -229,8 +229,8 @@ class IsRelativeTest(unittest.TestCase): True) def test_knownValues_is_relative_2(self): - from logilab.common import tree - self.assertEqual(modutils.is_relative('modutils', tree.__file__), + from xml.etree import ElementTree + self.assertEqual(modutils.is_relative('ElementPath', ElementTree.__file__), True) def test_knownValues_is_relative_3(self): @@ -242,24 +242,32 @@ class IsRelativeTest(unittest.TestCase): class GetModuleFilesTest(unittest.TestCase): def test_get_module_files_1(self): - """given a directory return a list of all available python module's files, even - in subdirectories - """ package = resources.find('data/find_test') modules = set(modutils.get_module_files(package, [])) + expected = ['__init__.py', 'module.py', 'module2.py', + 'noendingnewline.py', 'nonregr.py'] + self.assertEqual(modules, + {os.path.join(package, x) for x in expected}) + + def test_get_all_files(self): + """test that list_all returns all Python files from given location + """ + non_package = resources.find('data/notamodule') + modules = modutils.get_module_files(non_package, [], list_all=True) self.assertEqual( modules, - {os.path.join(package, x) for x in ['__init__.py', 'module.py', 'module2.py', 'noendingnewline.py', 'nonregr.py']}) + [os.path.join(non_package, 'file.py')], + ) def test_load_module_set_attribute(self): - import logilab.common.fileutils - import logilab - del logilab.common.fileutils - del sys.modules['logilab.common.fileutils'] - m = modutils.load_module_from_modpath(['logilab', 'common', 'fileutils']) - self.assertTrue(hasattr(logilab, 'common')) - self.assertTrue(hasattr(logilab.common, 'fileutils')) - self.assertTrue(m is logilab.common.fileutils) + import xml.etree.ElementTree + import xml + del xml.etree.ElementTree + del sys.modules['xml.etree.ElementTree'] + m = modutils.load_module_from_modpath(['xml', 'etree', 'ElementTree']) + self.assertTrue(hasattr(xml, 'etree')) + self.assertTrue(hasattr(xml.etree, 'ElementTree')) + self.assertTrue(m is xml.etree.ElementTree) if __name__ == '__main__': diff --git a/astroid/tests/unittest_nodes.py b/astroid/tests/unittest_nodes.py index 9c873e17..511a8c77 100644 --- a/astroid/tests/unittest_nodes.py +++ b/astroid/tests/unittest_nodes.py @@ -26,9 +26,11 @@ import six from astroid import bases from astroid import builder +from astroid import context as contextmod from astroid import exceptions from astroid import node_classes from astroid import nodes +from astroid import util from astroid import test_utils from astroid.tests import resources @@ -80,7 +82,6 @@ class AsStringTest(resources.SysPathSetup, unittest.TestCase): def test_module2_as_string(self): """check as_string on a whole module prepared to be returned identically """ - self.maxDiff = None module2 = resources.build_file('data/module2.py', 'data.module2') with open(resources.find('data/module2.py'), 'r') as fobj: self.assertMultiLineEqual(module2.as_string(), fobj.read()) @@ -302,14 +303,11 @@ class ImportNodeTest(resources.SysPathSetup, unittest.TestCase): self.assertEqual(myos.pytype(), '%s.module' % BUILTINS) def test_from_self_resolve(self): - pb = next(self.module.igetattr('pb')) - self.assertTrue(isinstance(pb, nodes.ClassDef), pb) - self.assertEqual(pb.root().name, 'logilab.common.shellutils') - self.assertEqual(pb.qname(), 'logilab.common.shellutils.ProgressBar') - if pb.newstyle: - self.assertEqual(pb.pytype(), '%s.type' % BUILTINS) - else: - self.assertEqual(pb.pytype(), '%s.classobj' % BUILTINS) + namenode = next(self.module.igetattr('NameNode')) + self.assertTrue(isinstance(namenode, nodes.ClassDef), namenode) + self.assertEqual(namenode.root().name, 'astroid.node_classes') + self.assertEqual(namenode.qname(), 'astroid.node_classes.Name') + self.assertEqual(namenode.pytype(), '%s.type' % BUILTINS) abspath = next(self.module2.igetattr('abspath')) self.assertTrue(isinstance(abspath, nodes.FunctionDef), abspath) self.assertEqual(abspath.root().name, 'os.path') @@ -373,11 +371,11 @@ from ..cave import wine\n\n""" # present in the other version. self.assertIsInstance(excs[0], nodes.ClassDef) self.assertEqual(excs[0].name, 'PickleError') - self.assertIs(excs[-1], bases.YES) + self.assertIs(excs[-1], util.YES) def test_absolute_import(self): astroid = resources.build_file('data/absimport.py') - ctx = bases.InferenceContext() + ctx = contextmod.InferenceContext() # will fail if absolute import failed ctx.lookupname = 'message' next(astroid['message'].infer(ctx)) diff --git a/astroid/tests/unittest_protocols.py b/astroid/tests/unittest_protocols.py index 12a5c609..c82fdd01 100644 --- a/astroid/tests/unittest_protocols.py +++ b/astroid/tests/unittest_protocols.py @@ -18,10 +18,10 @@ import unittest -from astroid import YES from astroid.test_utils import extract_node, require_version from astroid import InferenceError from astroid import nodes +from astroid import util from astroid.node_classes import AssignName, Const, Name, Starred @@ -67,7 +67,7 @@ class ProtocolTests(unittest.TestCase): for1_starred = next(assign_stmts.nodes_of_class(Starred)) assigned = next(for1_starred.assigned_stmts()) - self.assertEqual(assigned, YES) + self.assertEqual(assigned, util.YES) def _get_starred_stmts(self, code): assign_stmt = extract_node("{} #@".format(code)) @@ -108,16 +108,16 @@ class ProtocolTests(unittest.TestCase): @require_version(minver='3.0') def test_assigned_stmts_starred_yes(self): # Not something iterable and known - self._helper_starred_expected("a, *b = range(3) #@", YES) + self._helper_starred_expected("a, *b = range(3) #@", util.YES) # Not something inferrable - self._helper_starred_expected("a, *b = balou() #@", YES) + self._helper_starred_expected("a, *b = balou() #@", util.YES) # In function, unknown. self._helper_starred_expected(""" def test(arg): - head, *tail = arg #@""", YES) + head, *tail = arg #@""", util.YES) # These cases aren't worth supporting. self._helper_starred_expected( - "a, (*b, c), d = (1, (2, 3, 4), 5) #@", YES) + "a, (*b, c), d = (1, (2, 3, 4), 5) #@", util.YES) @require_version(minver='3.0') def test_assign_stmts_starred_fails(self): diff --git a/astroid/tests/unittest_regrtest.py b/astroid/tests/unittest_regrtest.py index a299e77a..ba7e2bf1 100644 --- a/astroid/tests/unittest_regrtest.py +++ b/astroid/tests/unittest_regrtest.py @@ -28,6 +28,8 @@ from astroid.raw_building import build_module from astroid.manager import AstroidManager from astroid.test_utils import require_version, extract_node from astroid.tests import resources +from astroid import transforms + class NonRegressionTests(resources.AstroidCacheSetupMixin, unittest.TestCase): @@ -35,6 +37,7 @@ class NonRegressionTests(resources.AstroidCacheSetupMixin, def setUp(self): sys.path.insert(0, resources.find('data')) MANAGER.always_load_extensions = True + MANAGER.astroid_cache[BUILTINS] = self._builtins def tearDown(self): # Since we may have created a brainless manager, leading @@ -53,7 +56,7 @@ class NonRegressionTests(resources.AstroidCacheSetupMixin, manager._failed_import_hooks = [] manager.astroid_cache = {} manager._mod_file_cache = {} - manager.transforms = {} + manager._transform = transforms.TransformVisitor() manager.clear_cache() # trigger proper bootstraping return manager diff --git a/astroid/tests/unittest_scoped_nodes.py b/astroid/tests/unittest_scoped_nodes.py index 1a1134fd..f7088d87 100644 --- a/astroid/tests/unittest_scoped_nodes.py +++ b/astroid/tests/unittest_scoped_nodes.py @@ -24,7 +24,10 @@ from functools import partial import unittest import warnings -from astroid import YES, builder, nodes, scoped_nodes +from astroid import builder +from astroid import nodes +from astroid import scoped_nodes +from astroid import util from astroid.exceptions import ( InferenceError, NotFoundError, NoDefault, ResolveError, MroError, @@ -86,9 +89,9 @@ class ModuleNodeTest(ModuleLoader, unittest.TestCase): red = next(self.module.igetattr('redirect')) self.assertIsInstance(red, nodes.FunctionDef) self.assertEqual(red.name, 'four_args') - pb = next(self.module.igetattr('pb')) - self.assertIsInstance(pb, nodes.ClassDef) - self.assertEqual(pb.name, 'ProgressBar') + namenode = next(self.module.igetattr('NameNode')) + self.assertIsInstance(namenode, nodes.Class) + self.assertEqual(namenode.name, 'Name') # resolve packageredirection mod = resources.build_file('data/appl/myConnection.py', 'data.appl.myConnection') @@ -1088,7 +1091,7 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase): instance = astroid['tgts'] # used to raise "'_Yes' object is not iterable", see # https://bitbucket.org/logilab/astroid/issue/17 - self.assertEqual(list(instance.infer()), [YES]) + self.assertEqual(list(instance.infer()), [util.YES]) def test_slots(self): astroid = builder.parse(""" @@ -1324,6 +1327,87 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase): type_cls = scoped_nodes.builtin_lookup("type")[1][0] self.assertEqual(cls.implicit_metaclass(), type_cls) + def test_implicit_metaclass_lookup(self): + cls = test_utils.extract_node(''' + class A(object): + pass + ''') + instance = cls.instanciate_class() + func = cls.getattr('mro') + self.assertEqual(len(func), 1) + self.assertRaises(NotFoundError, instance.getattr, 'mro') + + def test_metaclass_lookup_using_same_class(self): + # Check that we don't have recursive attribute access for metaclass + cls = test_utils.extract_node(''' + class A(object): pass + ''') + self.assertEqual(len(cls.getattr('mro')), 1) + + def test_metaclass_lookup_inferrence_errors(self): + module = builder.parse(''' + import six + + class Metaclass(type): + foo = lala + + @six.add_metaclass(Metaclass) + class B(object): pass + ''') + cls = module['B'] + self.assertEqual(util.YES, next(cls.igetattr('foo'))) + + def test_metaclass_lookup(self): + module = builder.parse(''' + import six + + class Metaclass(type): + foo = 42 + @classmethod + def class_method(cls): + pass + def normal_method(cls): + pass + @property + def meta_property(cls): + return 42 + @staticmethod + def static(): + pass + + @six.add_metaclass(Metaclass) + class A(object): + pass + ''') + acls = module['A'] + normal_attr = next(acls.igetattr('foo')) + self.assertIsInstance(normal_attr, nodes.Const) + self.assertEqual(normal_attr.value, 42) + + class_method = next(acls.igetattr('class_method')) + self.assertIsInstance(class_method, BoundMethod) + self.assertEqual(class_method.bound, module['Metaclass']) + + normal_method = next(acls.igetattr('normal_method')) + self.assertIsInstance(normal_method, BoundMethod) + self.assertEqual(normal_method.bound, module['A']) + + # Attribute access for properties: + # from the metaclass is a property object + # from the class that uses the metaclass, the value + # of the property + property_meta = next(module['Metaclass'].igetattr('meta_property')) + self.assertIsInstance(property_meta, UnboundMethod) + wrapping = scoped_nodes.get_wrapping_class(property_meta) + self.assertEqual(wrapping, module['Metaclass']) + + property_class = next(acls.igetattr('meta_property')) + self.assertIsInstance(property_class, nodes.Const) + self.assertEqual(property_class.value, 42) + + static = next(acls.igetattr('static')) + self.assertIsInstance(static, scoped_nodes.Function) + @test_utils.require_version(maxver='3.0') def test_implicit_metaclass_is_none(self): cls = test_utils.extract_node(""" @@ -1397,6 +1481,59 @@ class ClassNodeTest(ModuleLoader, unittest.TestCase): not_method = next(instance.igetattr('not_method')) self.assertIsInstance(not_method, scoped_nodes.Lambda) + def test_class_extra_decorators_frame_is_not_class(self): + ast_node = test_utils.extract_node(''' + def ala(): + def bala(): #@ + func = 42 + ''') + self.assertEqual(ast_node.extra_decorators, []) + + def test_class_extra_decorators_only_callfunc_are_considered(self): + ast_node = test_utils.extract_node(''' + class Ala(object): + def func(self): #@ + pass + func = 42 + ''') + self.assertEqual(ast_node.extra_decorators, []) + + def test_class_extra_decorators_only_assignment_names_are_considered(self): + ast_node = test_utils.extract_node(''' + class Ala(object): + def func(self): #@ + pass + def __init__(self): + self.func = staticmethod(func) + + ''') + self.assertEqual(ast_node.extra_decorators, []) + + def test_class_extra_decorators_only_same_name_considered(self): + ast_node = test_utils.extract_node(''' + class Ala(object): + def func(self): #@ + pass + bala = staticmethod(func) + ''') + self.assertEqual(ast_node.extra_decorators, []) + self.assertEqual(ast_node.type, 'method') + + def test_class_extra_decorators(self): + static_method, clsmethod = test_utils.extract_node(''' + class Ala(object): + def static(self): #@ + pass + def class_method(self): #@ + pass + class_method = classmethod(class_method) + static = staticmethod(static) + ''') + self.assertEqual(len(clsmethod.extra_decorators), 1) + self.assertEqual(clsmethod.type, 'classmethod') + self.assertEqual(len(static_method.extra_decorators), 1) + self.assertEqual(static_method.type, 'staticmethod') + if __name__ == '__main__': unittest.main() diff --git a/astroid/tests/unittest_transforms.py b/astroid/tests/unittest_transforms.py new file mode 100644 index 00000000..3bb0cc2d --- /dev/null +++ b/astroid/tests/unittest_transforms.py @@ -0,0 +1,218 @@ +# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of astroid. +# +# astroid is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 2.1 of the License, or (at your +# option) any later version. +# +# astroid 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 Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with astroid. If not, see <http://www.gnu.org/licenses/>.
+
+import contextlib
+import time
+import unittest
+
+from astroid import builder
+from astroid import nodes
+from astroid import parse
+from astroid import transforms
+
+
+@contextlib.contextmanager
+def add_transform(manager, node, transform, predicate=None):
+ manager.register_transform(node, transform, predicate)
+ try:
+ yield
+ finally:
+ manager.unregister_transform(node, transform, predicate)
+
+
+class TestTransforms(unittest.TestCase):
+
+ def setUp(self):
+ self.transformer = transforms.TransformVisitor()
+
+ def parse_transform(self, code):
+ module = parse(code, apply_transforms=False)
+ return self.transformer.visit(module)
+
+ def test_function_inlining_transform(self):
+ def transform_callfunc(node):
+ # Let's do some function inlining
+ inferred = next(node.infer())
+ return inferred
+
+ self.transformer.register_transform(nodes.CallFunc,
+ transform_callfunc)
+
+ module = self.parse_transform('''
+ def test(): return 42
+ test() #@
+ ''')
+
+ self.assertIsInstance(module.body[1], nodes.Discard)
+ self.assertIsInstance(module.body[1].value, nodes.Const)
+ self.assertEqual(module.body[1].value.value, 42)
+
+ def test_recursive_transforms_into_astroid_fields(self):
+ # Test that the transformer walks properly the tree
+ # by going recursively into the _astroid_fields per each node.
+ def transform_compare(node):
+ # Let's check the values of the ops
+ _, right = node.ops[0]
+ # Assume they are Consts and they were transformed before
+ # us.
+ return nodes.const_factory(node.left.value < right.value)
+
+ def transform_name(node):
+ # Should be Consts
+ return next(node.infer())
+
+ self.transformer.register_transform(nodes.Compare, transform_compare)
+ self.transformer.register_transform(nodes.Name, transform_name)
+
+ module = self.parse_transform('''
+ a = 42
+ b = 24
+ a < b
+ ''')
+
+ self.assertIsInstance(module.body[2], nodes.Discard)
+ self.assertIsInstance(module.body[2].value, nodes.Const)
+ self.assertFalse(module.body[2].value.value)
+
+ def test_transform_patches_locals(self):
+ def transform_function(node):
+ assign = nodes.Assign()
+ name = nodes.AssName()
+ name.name = 'value'
+ assign.targets = [name]
+ assign.value = nodes.const_factory(42)
+ node.body.append(assign)
+
+ self.transformer.register_transform(nodes.Function,
+ transform_function)
+
+ module = self.parse_transform('''
+ def test():
+ pass
+ ''')
+
+ func = module.body[0]
+ self.assertEqual(len(func.body), 2)
+ self.assertIsInstance(func.body[1], nodes.Assign)
+ self.assertEqual(func.body[1].as_string(), 'value = 42')
+
+ def test_predicates(self):
+ def transform_callfunc(node):
+ inferred = next(node.infer())
+ return inferred
+
+ def should_inline(node):
+ return node.func.name.startswith('inlineme')
+
+ self.transformer.register_transform(nodes.CallFunc,
+ transform_callfunc,
+ should_inline)
+
+ module = self.parse_transform('''
+ def inlineme_1():
+ return 24
+ def dont_inline_me():
+ return 42
+ def inlineme_2():
+ return 2
+ inlineme_1()
+ dont_inline_me()
+ inlineme_2()
+ ''')
+ values = module.body[-3:]
+ self.assertIsInstance(values[0], nodes.Discard)
+ self.assertIsInstance(values[0].value, nodes.Const)
+ self.assertEqual(values[0].value.value, 24)
+ self.assertIsInstance(values[1], nodes.Discard)
+ self.assertIsInstance(values[1].value, nodes.CallFunc)
+ self.assertIsInstance(values[2], nodes.Discard)
+ self.assertIsInstance(values[2].value, nodes.Const)
+ self.assertEqual(values[2].value.value, 2)
+
+ def test_transforms_are_separated(self):
+ # Test that the transforming is done at a separate
+ # step, which means that we are not doing inference
+ # on a partially constructred tree anymore, which was the
+ # source of crashes in the past when certain inference rules
+ # were used in a transform.
+ def transform_function(node):
+ if node.decorators:
+ for decorator in node.decorators.nodes:
+ inferred = next(decorator.infer())
+ if inferred.qname() == 'abc.abstractmethod':
+ return next(node.infer_call_result(node))
+
+ manager = builder.MANAGER
+ with add_transform(manager, nodes.Function, transform_function):
+ module = builder.parse('''
+ import abc
+ from abc import abstractmethod
+
+ class A(object):
+ @abc.abstractmethod
+ def ala(self):
+ return 24
+
+ @abstractmethod
+ def bala(self):
+ return 42
+ ''')
+
+ cls = module['A']
+ ala = cls.body[0]
+ bala = cls.body[1]
+ self.assertIsInstance(ala, nodes.Const)
+ self.assertEqual(ala.value, 24)
+ self.assertIsInstance(bala, nodes.Const)
+ self.assertEqual(bala.value, 42)
+
+ def test_transforms_are_called_for_builtin_modules(self):
+ # Test that transforms are called for builtin modules.
+ def transform_function(node):
+ name = nodes.AssName()
+ name.name = 'value'
+ node.args.args = [name]
+ return node
+
+ manager = builder.MANAGER
+ predicate = lambda node: node.root().name == 'time'
+ with add_transform(manager, nodes.Function,
+ transform_function, predicate):
+ builder_instance = builder.AstroidBuilder()
+ module = builder_instance.module_build(time)
+
+ asctime = module['asctime']
+ self.assertEqual(len(asctime.args.args), 1)
+ self.assertIsInstance(asctime.args.args[0], nodes.AssName)
+ self.assertEqual(asctime.args.args[0].name, 'value')
+
+ def test_builder_apply_transforms(self):
+ def transform_function(node):
+ return nodes.const_factory(42)
+
+ manager = builder.MANAGER
+ with add_transform(manager, nodes.Function, transform_function):
+ astroid_builder = builder.AstroidBuilder(apply_transforms=False)
+ module = astroid_builder.string_build('''def test(): pass''')
+
+ # The transform wasn't applied.
+ self.assertIsInstance(module.body[0], nodes.Function)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/astroid/transforms.py b/astroid/transforms.py new file mode 100644 index 00000000..5d8fc91b --- /dev/null +++ b/astroid/transforms.py @@ -0,0 +1,96 @@ +# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of astroid. +# +# astroid is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 2.1 of the License, or (at your +# option) any later version. +# +# astroid 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 Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with astroid. If not, see <http://www.gnu.org/licenses/>. + +import collections +import warnings + + +class TransformVisitor(object): + """A visitor for handling transforms. + + The standard approach of using it is to call + :meth:`~visit` with an *astroid* module and the class + will take care of the rest, walking the tree and running the + transforms for each encountered node. + """ + + def __init__(self): + self.transforms = collections.defaultdict(list) + + def _transform(self, node): + """Call matching transforms for the given node if any and return the + transformed node. + """ + cls = node.__class__ + if cls not in self.transforms: + # no transform registered for this class of node + return node + + transforms = self.transforms[cls] + orig_node = node # copy the reference + for transform_func, predicate in transforms: + if predicate is None or predicate(node): + ret = transform_func(node) + # if the transformation function returns something, it's + # expected to be a replacement for the node + if ret is not None: + if node is not orig_node: + # node has already be modified by some previous + # transformation, warn about it + warnings.warn('node %s substituted multiple times' % node) + node = ret + return node + + def _visit(self, node): + if hasattr(node, '_astroid_fields'): + for field in node._astroid_fields: + value = getattr(node, field) + visited = self._visit_generic(value) + setattr(node, field, visited) + return self._transform(node) + + def _visit_generic(self, node): + if isinstance(node, list): + return [self._visit_generic(child) for child in node] + elif isinstance(node, tuple): + return tuple(self._visit_generic(child) for child in node) + else: + return self._visit(node) + + def register_transform(self, node_class, transform, predicate=None): + """Register `transform(node)` function to be applied on the given + astroid's `node_class` if `predicate` is None or returns true + when called with the node as argument. + + The transform function may return a value which is then used to + substitute the original node in the tree. + """ + self.transforms[node_class].append((transform, predicate)) + + def unregister_transform(self, node_class, transform, predicate=None): + """Unregister the given transform.""" + self.transforms[node_class].remove((transform, predicate)) + + def visit(self, module): + """Walk the given astroid *tree* and transform each encountered node + + Only the nodes which have transforms registered will actually + be replaced or changed. + """ + module.body = [self._visit(child) for child in module.body] + return self._transform(module) diff --git a/astroid/util.py b/astroid/util.py new file mode 100644 index 00000000..8dde9b97 --- /dev/null +++ b/astroid/util.py @@ -0,0 +1,38 @@ +# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr +# +# This file is part of astroid. +# +# astroid is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by the +# Free Software Foundation, either version 2.1 of the License, or (at your +# option) any later version. +# +# astroid 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 Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License along +# with astroid. If not, see <http://www.gnu.org/licenses/>. +# +# The code in this file was originally part of logilab-common, licensed under +# the same license. + +class _Yes(object): + """Special inference object, which is returned when inference fails.""" + def __repr__(self): + return 'YES' + + def __getattribute__(self, name): + if name == 'next': + raise AttributeError('next method should not be called') + if name.startswith('__') and name.endswith('__'): + return super(_Yes, self).__getattribute__(name) + return self + + def __call__(self, *args, **kwargs): + return self + + +YES = _Yes() @@ -1,22 +1,20 @@ -[tox]
-# official list is
-# envlist = py27, py33, py34, pypy, jython
-# envlist = py27, py34, pylint
-envlist = py27, py33
-
-[testenv:pylint]
-deps =
- lazy-object-proxy
- logilab-common
- six
- hg+https://bitbucket.org/logilab/astroid
- hg+https://bitbucket.org/logilab/pylint
-commands = pylint -rn --rcfile={toxinidir}/pylintrc {envsitepackagesdir}/astroid
-
-[testenv]
-deps =
- lazy-object-proxy
- logilab-common
- six
-
-commands = python -m unittest discover -s {envsitepackagesdir}/astroid/tests -p "unittest*.py"
+[tox] +# official list is +# envlist = py27, py33, py34, pypy, jython +# envlist = py27, py34, pylint +envlist = py27, py33 + +[testenv:pylint] +deps = + lazy-object-proxy + six + hg+https://bitbucket.org/logilab/astroid + hg+https://bitbucket.org/logilab/pylint +commands = pylint -rn --rcfile={toxinidir}/pylintrc {envsitepackagesdir}/astroid + +[testenv] +deps = + lazy-object-proxy + six + +commands = python -m unittest discover -s {envsitepackagesdir}/astroid/tests -p "unittest*.py" |