# 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 . """This module contains the classes for "scoped" node, i.e. which are opening a new local scope in the language definition : Module, Class, Function (and Lambda, GenExpr, DictComp and SetComp to some extent). """ from __future__ import with_statement __doctype__ = "restructuredtext en" import sys import warnings from itertools import chain try: from io import BytesIO except ImportError: from cStringIO import StringIO as BytesIO import six from logilab.common.compat import builtins from logilab.common.decorators import cached, cachedproperty from astroid.exceptions import NotFoundError, \ AstroidBuildingException, InferenceError, ResolveError from astroid.node_classes import Const, DelName, DelAttr, \ Dict, From, List, Pass, Raise, Return, Tuple, Yield, YieldFrom, \ LookupMixIn, const_factory as cf, unpack_infer, Name, CallFunc from astroid.bases import NodeNG, InferenceContext, Instance,\ YES, Generator, UnboundMethod, BoundMethod, _infer_stmts, \ BUILTINS from astroid.mixins import FilterStmtsMixin from astroid.bases import Statement from astroid.manager import AstroidManager ITER_METHODS = ('__iter__', '__getitem__') PY3K = sys.version_info >= (3, 0) def _c3_merge(sequences): """Merges MROs in *sequences* to a single MRO using the C3 algorithm. Adapted from http://www.python.org/download/releases/2.3/mro/. """ result = [] while True: sequences = [s for s in sequences if s] # purge empty sequences if not sequences: return result for s1 in sequences: # find merge candidates among seq heads candidate = s1[0] for s2 in sequences: if candidate in s2[1:]: candidate = None break # reject the current head, it appears later else: break if not candidate: # Show all the remaining bases, which were considered as # candidates for the next mro sequence. bases = ["({})".format(", ".join(base.name for base in subsequence)) for subsequence in sequences] raise ResolveError("Cannot create a consistent method resolution " "order for bases %s" % ", ".join(bases)) result.append(candidate) # remove the chosen candidate for seq in sequences: if seq[0] == candidate: del seq[0] def _verify_duplicates_mro(sequences): for sequence in sequences: names = [node.qname() for node in sequence] if len(names) != len(set(names)): raise ResolveError('Duplicates found in the mro.') def remove_nodes(func, cls): def wrapper(*args, **kwargs): nodes = [n for n in func(*args, **kwargs) if not isinstance(n, cls)] if not nodes: raise NotFoundError() return nodes return wrapper def function_to_method(n, klass): if isinstance(n, Function): if n.type == 'classmethod': return BoundMethod(n, klass) if n.type != 'staticmethod': return UnboundMethod(n) return n def std_special_attributes(self, name, add_locals=True): if add_locals: locals = self.locals else: locals = {} if name == '__name__': return [cf(self.name)] + locals.get(name, []) if name == '__doc__': return [cf(self.doc)] + locals.get(name, []) if name == '__dict__': return [Dict()] + locals.get(name, []) raise NotFoundError(name) MANAGER = AstroidManager() def builtin_lookup(name): """lookup a name into the builtin module return the list of matching statements and the astroid for the builtin module """ builtin_astroid = MANAGER.ast_from_module(builtins) if name == '__dict__': return builtin_astroid, () try: stmts = builtin_astroid.locals[name] except KeyError: stmts = () return builtin_astroid, stmts # TODO move this Mixin to mixins.py; problem: 'Function' in _scope_lookup class LocalsDictNodeNG(LookupMixIn, NodeNG): """ this class provides locals handling common to Module, Function and Class nodes, including a dict like interface for direct access to locals information """ # attributes below are set by the builder module or by raw factories # dictionary of locals with name as key and node defining the local as # value def qname(self): """return the 'qualified' name of the node, eg module.name, module.class.name ... """ if self.parent is None: return self.name return '%s.%s' % (self.parent.frame().qname(), self.name) def frame(self): """return the first parent frame node (i.e. Module, Function or Class) """ return self def scope(self): """return the first node defining a new scope (i.e. Module, Function, Class, Lambda but also GenExpr, DictComp and SetComp) """ return self def _scope_lookup(self, node, name, offset=0): """XXX method for interfacing the scope lookup""" try: stmts = node._filter_stmts(self.locals[name], self, offset) except KeyError: stmts = () if stmts: return self, stmts if self.parent: # i.e. not Module # nested scope: if parent scope is a function, that's fine # else jump to the module pscope = self.parent.scope() if not pscope.is_function: pscope = pscope.root() return pscope.scope_lookup(node, name) return builtin_lookup(name) # Module def set_local(self, name, stmt): """define in locals ( is the node defining the name) if the node is a Module node (i.e. has globals), add the name to globals if the name is already defined, ignore it """ #assert not stmt in self.locals.get(name, ()), (self, stmt) self.locals.setdefault(name, []).append(stmt) __setitem__ = set_local def _append_node(self, child): """append a child, linking it in the tree""" self.body.append(child) child.parent = self def add_local_node(self, child_node, name=None): """append a child which should alter locals to the given node""" if name != '__class__': # add __class__ node as a child will cause infinite recursion later! self._append_node(child_node) self.set_local(name or child_node.name, child_node) def __getitem__(self, item): """method from the `dict` interface returning the first node associated with the given name in the locals dictionary :type item: str :param item: the name of the locally defined object :raises KeyError: if the name is not defined """ return self.locals[item][0] def __iter__(self): """method from the `dict` interface returning an iterator on `self.keys()` """ return iter(self.keys()) def keys(self): """method from the `dict` interface returning a tuple containing locally defined names """ return list(self.locals.keys()) def values(self): """method from the `dict` interface returning a tuple containing locally defined nodes which are instance of `Function` or `Class` """ return [self[key] for key in self.keys()] def items(self): """method from the `dict` interface returning a list of tuple containing each locally defined name with its associated node, which is an instance of `Function` or `Class` """ return list(zip(self.keys(), self.values())) def __contains__(self, name): return name in self.locals has_key = __contains__ # Module ##################################################################### class Module(LocalsDictNodeNG): _astroid_fields = ('body',) fromlineno = 0 lineno = 0 # attributes below are set by the builder module or by raw factories # the file from which as been extracted the astroid representation. It may # be None if the representation has been built from a built-in module file = None # Alternatively, if built from a string/bytes, this can be set file_bytes = None # encoding of python source file, so we can get unicode out of it (python2 # only) file_encoding = None # the module name name = None # boolean for astroid built from source (i.e. ast) pure_python = None # boolean for package module package = None # dictionary of globals with name as key and node defining the global # as value globals = None # Future imports future_imports = None # names of python special attributes (handled by getattr impl.) special_attributes = set(('__name__', '__doc__', '__file__', '__path__', '__dict__')) # names of module attributes available through the global scope scope_attrs = set(('__name__', '__doc__', '__file__', '__path__')) def __init__(self, name, doc, pure_python=True): self.name = name self.doc = doc self.pure_python = pure_python self.locals = self.globals = {} self.body = [] self.future_imports = set() def _get_stream(self): if self.file_bytes is not None: return BytesIO(self.file_bytes) if self.file is not None: stream = open(self.file, 'rb') return stream return None @property def file_stream(self): warnings.warn("file_stream property is deprecated and " "it is slated for removal in astroid 1.6." "Use the new method 'stream' instead.", PendingDeprecationWarning, stacklevel=2) return self._get_stream() def stream(self): """Get a stream to the underlying file or bytes.""" return self._get_stream() def close(self): """Close the underlying file streams.""" warnings.warn("close method is deprecated and it is " "slated for removal in astroid 1.6, along " "with 'file_stream' property. " "Its behaviour is replaced by managing each " "file stream returned by the 'stream' method.", PendingDeprecationWarning, stacklevel=2) def block_range(self, lineno): """return block line numbers. start from the beginning whatever the given lineno """ return self.fromlineno, self.tolineno def scope_lookup(self, node, name, offset=0): if name in self.scope_attrs and not name in self.locals: try: return self, self.getattr(name) except NotFoundError: return self, () return self._scope_lookup(node, name, offset) def pytype(self): return '%s.module' % BUILTINS def display_type(self): return 'Module' def getattr(self, name, context=None, ignore_locals=False): if name in self.special_attributes: if name == '__file__': return [cf(self.file)] + self.locals.get(name, []) if name == '__path__' and self.package: return [List()] + self.locals.get(name, []) return std_special_attributes(self, name) if not ignore_locals and name in self.locals: return self.locals[name] if self.package: try: return [self.import_module(name, relative_only=True)] except AstroidBuildingException: raise NotFoundError(name) except SyntaxError: raise NotFoundError(name) except Exception:# XXX pylint tests never pass here; do we need it? import traceback traceback.print_exc() raise NotFoundError(name) getattr = remove_nodes(getattr, DelName) def igetattr(self, name, context=None): """inferred getattr""" # set lookup name since this is necessary to infer on import nodes for # instance if not context: context = InferenceContext() try: return _infer_stmts(self.getattr(name, context), context, frame=self, lookupname=name) except NotFoundError: raise InferenceError(name) def fully_defined(self): """return True if this module has been built from a .py file and so contains a complete representation including the code """ return self.file is not None and self.file.endswith('.py') def statement(self): """return the first parent node marked as statement node consider a module as a statement... """ return self def previous_sibling(self): """module has no sibling""" return def next_sibling(self): """module has no sibling""" return if sys.version_info < (2, 8): @cachedproperty def _absolute_import_activated(self): for stmt in self.locals.get('absolute_import', ()): if isinstance(stmt, From) and stmt.modname == '__future__': return True return False else: _absolute_import_activated = True def absolute_import_activated(self): return self._absolute_import_activated def import_module(self, modname, relative_only=False, level=None): """import the given module considering self as context""" if relative_only and level is None: level = 0 absmodname = self.relative_to_absolute_name(modname, level) try: return MANAGER.ast_from_module_name(absmodname) except AstroidBuildingException: # we only want to import a sub module or package of this module, # skip here if relative_only: raise return MANAGER.ast_from_module_name(modname) def relative_to_absolute_name(self, modname, level): """return the absolute module name for a relative import. The relative import can be implicit or explicit. """ # XXX this returns non sens when called on an absolute import # like 'pylint.checkers.astroid.utils' # XXX doesn't return absolute name if self.name isn't absolute name if self.absolute_import_activated() and level is None: return modname if level: if self.package: level = level - 1 package_name = self.name.rsplit('.', level)[0] elif self.package: package_name = self.name else: package_name = self.name.rsplit('.', 1)[0] if package_name: if not modname: return package_name return '%s.%s' % (package_name, modname) return modname def wildcard_import_names(self): """return the list of imported names when this module is 'wildcard imported' It doesn't include the '__builtins__' name which is added by the current CPython implementation of wildcard imports. """ # take advantage of a living module if it exists try: living = sys.modules[self.name] except KeyError: pass else: try: return living.__all__ except AttributeError: return [name for name in living.__dict__.keys() if not name.startswith('_')] # else lookup the 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('_')] try: all = self['__all__'] except KeyError: return default try: explicit = next(all.assigned_stmts()) except InferenceError: return default except AttributeError: # not an assignment node # XXX infer? return default # Try our best to detect the exported name. infered = [] try: explicit = next(explicit.infer()) except InferenceError: return default if not isinstance(explicit, (Tuple, List)): return default str_const = lambda node: (isinstance(node, Const) and isinstance(node.value, six.string_types)) for node in explicit.elts: if str_const(node): infered.append(node.value) else: try: infered_node = next(node.infer()) except InferenceError: continue if str_const(infered_node): infered.append(infered_node.value) return infered class ComprehensionScope(LocalsDictNodeNG): def frame(self): return self.parent.frame() scope_lookup = LocalsDictNodeNG._scope_lookup class GenExpr(ComprehensionScope): _astroid_fields = ('elt', 'generators') def __init__(self): self.locals = {} self.elt = None self.generators = [] class DictComp(ComprehensionScope): _astroid_fields = ('key', 'value', 'generators') def __init__(self): self.locals = {} self.key = None self.value = None self.generators = [] class SetComp(ComprehensionScope): _astroid_fields = ('elt', 'generators') def __init__(self): self.locals = {} self.elt = None self.generators = [] class _ListComp(NodeNG): """class representing a ListComp node""" _astroid_fields = ('elt', 'generators') elt = None generators = None if sys.version_info >= (3, 0): class ListComp(_ListComp, ComprehensionScope): """class representing a ListComp node""" def __init__(self): self.locals = {} else: class ListComp(_ListComp): """class representing a ListComp node""" # Function ################################################################### def _infer_decorator_callchain(node): """Detect decorator call chaining and see if the end result is a static or a classmethod. """ if not isinstance(node, Function): return if not node.parent: return try: # TODO: We don't handle multiple inference results right now, # because there's no flow to reason when the return # is what we are looking for, a static or a class method. result = next(node.infer_call_result(node.parent)) except (StopIteration, InferenceError): return if isinstance(result, Instance): result = result._proxied if isinstance(result, Class): if result.is_subtype_of('%s.classmethod' % BUILTINS): return 'classmethod' if result.is_subtype_of('%s.staticmethod' % BUILTINS): return 'staticmethod' def _function_type(self): """ Function 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, CallFunc): # Handle the following case: # @some_decorator(arg1, arg2) # def func(...) # try: current = next(node.func.infer()) except 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 InferenceError: pass return self._type class Lambda(LocalsDictNodeNG, FilterStmtsMixin): _astroid_fields = ('args', 'body',) name = '' # function's type, 'function' | 'method' | 'staticmethod' | 'classmethod' type = 'function' def __init__(self): self.locals = {} self.args = [] self.body = [] def pytype(self): if 'method' in self.type: return '%s.instancemethod' % BUILTINS return '%s.function' % BUILTINS def display_type(self): if 'method' in self.type: return 'Method' return 'Function' def callable(self): return True def argnames(self): """return a list of argument names""" if self.args.args: # maybe None with builtin functions names = _rec_get_names(self.args.args) else: names = [] if self.args.vararg: names.append(self.args.vararg) if self.args.kwarg: names.append(self.args.kwarg) return names def infer_call_result(self, caller, context=None): """infer what a function is returning when called""" return self.body.infer(context) def scope_lookup(self, node, name, offset=0): if node in self.args.defaults or node in self.args.kw_defaults: frame = self.parent.frame() # line offset to avoid that def func(f=func) resolve the default # value to the defined function offset = -1 else: # check this is not used in function decorators frame = self return frame._scope_lookup(node, name, offset) class Function(Statement, Lambda): if PY3K: _astroid_fields = ('decorators', 'args', 'body', 'returns') returns = None else: _astroid_fields = ('decorators', 'args', 'body') special_attributes = set(('__name__', '__doc__', '__dict__')) is_function = True # attributes below are set by the builder module or by raw factories blockstart_tolineno = None decorators = None _type = "function" type = cachedproperty(_function_type) def __init__(self, name, doc): self.locals = {} self.args = [] self.body = [] self.name = name self.doc = doc self.extra_decorators = [] self.instance_attrs = {} @cachedproperty def fromlineno(self): # lineno is the line number of the first decorator, we want the def # statement lineno lineno = self.lineno if self.decorators is not None: lineno += sum(node.tolineno - node.lineno + 1 for node in self.decorators.nodes) return lineno @cachedproperty def blockstart_tolineno(self): return self.args.tolineno def block_range(self, lineno): """return block line numbers. start from the "def" position whatever the given lineno """ return self.fromlineno, self.tolineno 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. """ if name == '__module__': return [cf(self.root().qname())] if name in self.instance_attrs: return self.instance_attrs[name] return std_special_attributes(self, name, False) def is_method(self): """return true if the function node should be considered as a method""" # check we are defined in a Class, because this is usually expected # (e.g. pylint...) when is_method() return True return self.type != 'function' and isinstance(self.parent.frame(), Class) def decoratornames(self): """return a list of decorator qualified names""" result = set() decoratornodes = [] if self.decorators is not None: decoratornodes += self.decorators.nodes decoratornodes += self.extra_decorators for decnode in decoratornodes: for infnode in decnode.infer(): result.add(infnode.qname()) return result decoratornames = cached(decoratornames) def is_bound(self): """return true if the function is bound to an Instance or a class""" return self.type == 'classmethod' def is_abstract(self, pass_is_abstract=True): """Returns True if the method is abstract. A method is considered abstract if - the only statement is 'raise NotImplementedError', or - the only statement is 'pass' and pass_is_abstract is True, or - the method is annotated with abc.astractproperty/abc.abstractmethod """ if self.decorators: for node in self.decorators.nodes: try: infered = next(node.infer()) except InferenceError: continue if infered and infered.qname() in ('abc.abstractproperty', 'abc.abstractmethod'): return True for child_node in self.body: if isinstance(child_node, Raise): if child_node.raises_not_implemented(): return True if pass_is_abstract and isinstance(child_node, Pass): return True return False # empty function is the same as function with a single "pass" statement if pass_is_abstract: return True def is_generator(self): """return true if this is a generator function""" # XXX should be flagged, not computed return next(self.nodes_of_class((Yield, YieldFrom), skip_klass=(Function, Lambda)), False) def infer_call_result(self, caller, context=None): """infer what a function is returning when called""" if self.is_generator(): yield Generator() return # This is really a gigantic hack to work around metaclass generators # that return transient class-generating functions. Pylint's AST structure # cannot handle a base class object that is only used for calling __new__, # but does not contribute to the inheritance structure itself. We inject # a fake class into the hierarchy here for several well-known metaclass # generators, and filter it out later. if (self.name == 'with_metaclass' and len(self.args.args) == 1 and self.args.vararg is not None): metaclass = next(caller.args[0].infer(context)) if isinstance(metaclass, Class): c = Class('temporary_class', None) c.hide = True c.parent = self c.bases = [next(b.infer(context)) for b in caller.args[1:]] c._metaclass = metaclass yield c return returns = self.nodes_of_class(Return, skip_klass=Function) for returnnode in returns: if returnnode.value is None: yield Const(None) else: try: for infered in returnnode.value.infer(context): yield infered except InferenceError: yield YES def _rec_get_names(args, names=None): """return a list of all argument names""" if names is None: names = [] for arg in args: if isinstance(arg, Tuple): _rec_get_names(arg.elts, names) else: names.append(arg.name) return names # Class ###################################################################### def _is_metaclass(klass, seen=None): """ Return if the given class can be used as a metaclass. """ if klass.name == 'type': return True if seen is None: seen = set() for base in klass.bases: try: for baseobj in base.infer(): if baseobj in seen: continue else: seen.add(baseobj) if isinstance(baseobj, Instance): # not abstract return False if baseobj is YES: continue if baseobj is klass: continue if not isinstance(baseobj, Class): continue if baseobj._type == 'metaclass': return True if _is_metaclass(baseobj, seen): return True except InferenceError: continue return False def _class_type(klass, ancestors=None): """return a Class node type to differ metaclass, interface and exception from 'regular' classes """ # XXX we have to store ancestors in case we have a ancestor loop if klass._type is not None: return klass._type if _is_metaclass(klass): klass._type = 'metaclass' elif klass.name.endswith('Interface'): klass._type = 'interface' elif klass.name.endswith('Exception'): klass._type = 'exception' else: if ancestors is None: ancestors = set() if klass in ancestors: # XXX we are in loop ancestors, and have found no type klass._type = 'class' return 'class' ancestors.add(klass) for base in klass.ancestors(recurs=False): name = _class_type(base, ancestors) if name != 'class': if name == 'metaclass' and not _is_metaclass(klass): # don't propagate it if the current class # can't be a metaclass continue klass._type = base.type break if klass._type is None: klass._type = 'class' return klass._type def _iface_hdlr(iface_node): """a handler function used by interfaces to handle suspicious interface nodes """ return True class Class(Statement, LocalsDictNodeNG, FilterStmtsMixin): # some of the attributes below are set by the builder module or # by a raw factories # a dictionary of class instances attributes _astroid_fields = ('decorators', 'bases', 'body') # name decorators = None special_attributes = set(('__name__', '__doc__', '__dict__', '__module__', '__bases__', '__mro__', '__subclasses__')) blockstart_tolineno = None _type = None _metaclass_hack = False hide = False type = property(_class_type, doc="class'type, possible values are 'class' | " "'metaclass' | 'interface' | 'exception'") def __init__(self, name, doc): self.instance_attrs = {} self.locals = {} self.bases = [] self.body = [] self.name = name self.doc = doc def _newstyle_impl(self, context=None): if context is None: context = 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() # 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, Class): self._newstyle = klass._newstyle_impl(context) if self._newstyle is None: self._newstyle = False return self._newstyle _newstyle = None newstyle = property(_newstyle_impl, doc="boolean indicating if it's a new style class" "or not") @cachedproperty def blockstart_tolineno(self): if self.bases: return self.bases[-1].tolineno else: return self.fromlineno def block_range(self, lineno): """return block line numbers. start from the "class" position whatever the given lineno """ return self.fromlineno, self.tolineno def pytype(self): if self.newstyle: return '%s.type' % BUILTINS return '%s.classobj' % BUILTINS def display_type(self): return 'Class' def callable(self): return True def is_subtype_of(self, type_name, context=None): if self.qname() == type_name: return True for anc in self.ancestors(context=context): if anc.qname() == type_name: return True def infer_call_result(self, caller, context=None): """infer what a class is returning when called""" if self.is_subtype_of('%s.type' % (BUILTINS,), context) and len(caller.args) == 3: name_node = next(caller.args[0].infer(context)) if (isinstance(name_node, Const) and isinstance(name_node.value, six.string_types)): name = name_node.value else: yield YES return result = Class(name, None) bases = next(caller.args[1].infer(context)) if isinstance(bases, (Tuple, List)): result.bases = bases.itered() else: # 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. yield YES return result.parent = caller.parent yield result else: yield Instance(self) def scope_lookup(self, node, name, offset=0): if node in self.bases: frame = self.parent.frame() # line offset to avoid that class A(A) resolve the ancestor to # the defined class offset = -1 else: frame = self return frame._scope_lookup(node, name, offset) # list of parent class as a list of string (i.e. names as they appear # in the class definition) XXX bw compat def basenames(self): return [bnode.as_string() for bnode in self.bases] basenames = property(basenames) def ancestors(self, recurs=True, context=None): """return an iterator on the node base classes in a prefixed depth first order :param recurs: boolean indicating if it should recurse or return direct ancestors only """ # FIXME: should be possible to choose the resolution order # FIXME: inference make infinite loops possible here yielded = set([self]) if context is None: context = InferenceContext() if sys.version_info[0] >= 3: if not self.bases and self.qname() != 'builtins.object': yield builtin_lookup("object")[1][0] return for stmt in self.bases: try: for baseobj in stmt.infer(context): if not isinstance(baseobj, Class): if isinstance(baseobj, Instance): baseobj = baseobj._proxied else: # duh ? continue if not baseobj.hide: if baseobj in yielded: continue # cf xxx above yielded.add(baseobj) yield baseobj if recurs: for grandpa in baseobj.ancestors(recurs=True, context=context): if grandpa in yielded: continue # cf xxx above yielded.add(grandpa) yield grandpa except InferenceError: # XXX log error ? continue def local_attr_ancestors(self, name, context=None): """return an iterator on astroid representation of parent classes which have defined in their locals """ for astroid in self.ancestors(context=context): if name in astroid: yield astroid def instance_attr_ancestors(self, name, context=None): """return an iterator on astroid representation of parent classes which have defined in their instance attribute dictionary """ for astroid in self.ancestors(context=context): if name in astroid.instance_attrs: yield astroid def has_base(self, node): return node in self.bases def local_attr(self, name, context=None): """return the list of assign node associated to name in this class locals or in its parents :raises `NotFoundError`: if no attribute with this name has been find in this class or its parent classes """ try: return self.locals[name] except KeyError: # get if from the first parent implementing it if any for class_node in self.local_attr_ancestors(name, context): return class_node.locals[name] raise NotFoundError(name) local_attr = remove_nodes(local_attr, DelAttr) def instance_attr(self, name, context=None): """return the astroid nodes associated to name in this class instance attributes dictionary and in its parents :raises `NotFoundError`: if no attribute with this name has been find in this class or its parent classes """ # Return a copy, so we don't modify self.instance_attrs, # which could lead to infinite loop. values = list(self.instance_attrs.get(name, [])) # get all values from parents for class_node in self.instance_attr_ancestors(name, context): values += class_node.instance_attrs[name] if not values: raise NotFoundError(name) return values instance_attr = remove_nodes(instance_attr, DelAttr) def instanciate_class(self): """return Instance of Class node, else return self""" return 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. It may return a YES object if the attribute has not been actually found but a __getattr__ or __getattribute__ method is defined """ values = self.locals.get(name, []) if name in self.special_attributes: if name == '__module__': return [cf(self.root().qname())] + values # FIXME: do we really need the actual list of ancestors? # returning [Tuple()] + values don't break any test # this is ticket http://www.logilab.org/ticket/52785 # XXX need proper meta class handling + MRO implementation if name == '__bases__' or (name == '__mro__' and self.newstyle): node = Tuple() node.items = self.ancestors(recurs=True, context=context) return [node] + values return std_special_attributes(self, name) # don't modify the list in self.locals! values = list(values) for classnode in self.ancestors(recurs=True, context=context): values += classnode.locals.get(name, []) if not values: raise NotFoundError(name) return values 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 if not context: context = InferenceContext() try: for infered in _infer_stmts(self.getattr(name, context), context, frame=self, lookupname=name): # yield YES object instead of descriptors when necessary if not isinstance(infered, Const) and isinstance(infered, Instance): try: infered._proxied.getattr('__get__', context) except NotFoundError: yield infered else: yield YES else: yield function_to_method(infered, self) except NotFoundError: if not name.startswith('__') and self.has_dynamic_getattr(context): # class handle some dynamic attributes, return a YES object yield YES else: raise InferenceError(name) def has_dynamic_getattr(self, context=None): """return True if the class has a custom __getattr__ or __getattribute__ method """ # need to explicitly handle optparse.Values (setattr is not detected) if self.name == 'Values' and self.root().name == 'optparse': return True try: self.getattr('__getattr__', context) return True except NotFoundError: #if self.newstyle: XXX cause an infinite recursion error try: getattribute = self.getattr('__getattribute__', context)[0] if getattribute.root().name != BUILTINS: # class has a custom __getattribute__ defined return True except NotFoundError: pass return False def methods(self): """return an iterator on all methods defined in the class and its ancestors """ done = {} for astroid in chain(iter((self,)), self.ancestors()): for meth in astroid.mymethods(): if meth.name in done: continue done[meth.name] = None yield meth def mymethods(self): """return an iterator on all methods defined in the class""" for member in self.values(): if isinstance(member, Function): yield member def interfaces(self, herited=True, handler_func=_iface_hdlr): """return an iterator on interfaces implemented by the given class node """ # FIXME: what if __implements__ = (MyIFace, MyParent.__implements__)... try: implements = Instance(self).getattr('__implements__')[0] except NotFoundError: return if not herited and not implements.frame() is self: return found = set() missing = False for iface in unpack_infer(implements): if iface is YES: missing = True continue if not iface in found and handler_func(iface): found.add(iface) yield iface if missing: raise InferenceError() _metaclass = None def _explicit_metaclass(self): """ Return the explicit defined metaclass for the current class. An explicit defined 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 no explicit bases but there is a global ``__metaclass__`` variable. """ for base in self.bases: try: for baseobj in base.infer(): if isinstance(baseobj, Class) and baseobj.hide: self._metaclass = baseobj._metaclass self._metaclass_hack = True break except InferenceError: pass if self._metaclass: # Expects this from Py3k TreeRebuilder try: return next(node for node in self._metaclass.infer() if node is not YES) except (InferenceError, StopIteration): return None if sys.version_info >= (3, ): return None if '__metaclass__' in self.locals: assignment = self.locals['__metaclass__'][-1] elif self.bases: return None elif '__metaclass__' in self.root().locals: assignments = [ass for ass in self.root().locals['__metaclass__'] if ass.lineno < self.lineno] if not assignments: return None assignment = assignments[-1] else: return None try: infered = next(assignment.infer()) except InferenceError: return if infered is YES: # don't expose this return None return infered def metaclass(self): """ 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() if klass is None: for parent in self.ancestors(): klass = parent.metaclass() if klass is not None: break return klass def has_metaclass_hack(self): return self._metaclass_hack def _islots(self): """ Return an iterator with the inferred slots. """ if '__slots__' not in self.locals: return for slots in self.igetattr('__slots__'): # check if __slots__ is a valid type for meth in ITER_METHODS: try: slots.getattr(meth) break except NotFoundError: continue else: continue if isinstance(slots, Const): # a string. Ignore the following checks, # but yield the node, only if it has a value if slots.value: yield slots continue if not hasattr(slots, 'itered'): # we can't obtain the values, maybe a .deque? continue if isinstance(slots, Dict): values = [item[0] for item in slots.items] else: values = slots.itered() if values is YES: continue for elt in values: try: for infered in elt.infer(): if infered is YES: continue if (not isinstance(infered, Const) or not isinstance(infered.value, six.string_types)): continue if not infered.value: continue yield infered except InferenceError: continue # Cached, because inferring them all the time is expensive @cached def slots(self): """Get all the slots for this node. If the class doesn't define any slot, through `__slots__` variable, then this function will return a None. Also, it will return None in the case the slots weren't inferred. Otherwise, it will return a list of slot names. """ slots = self._islots() try: first = next(slots) except StopIteration: # The class doesn't have a __slots__ definition. return None return [first] + list(slots) def _inferred_bases(self, recurs=True, context=None): # TODO(cpopa): really similar with .ancestors, # but the difference is when one base is inferred, # only the first object is wanted. That's because # we aren't interested in superclasses, as in the following # example: # # class SomeSuperClass(object): pass # class SomeClass(SomeSuperClass): pass # class Test(SomeClass): pass # # Inferring SomeClass from the Test's bases will give # us both SomeClass and SomeSuperClass, but we are interested # only in SomeClass. if context is None: context = InferenceContext() if sys.version_info[0] >= 3: if not self.bases and self.qname() != 'builtins.object': yield builtin_lookup("object")[1][0] return for stmt in self.bases: try: baseobj = next(stmt.infer(context=context)) except InferenceError: # XXX log error ? continue if isinstance(baseobj, Instance): baseobj = baseobj._proxied if not isinstance(baseobj, Class): continue if not baseobj.hide: yield baseobj def mro(self, context=None): """Get the method resolution order, using C3 linearization. It returns the list of ancestors sorted by the mro. This will raise `NotImplementedError` for old-style classes, since they don't have the concept of MRO. """ if not self.newstyle: raise NotImplementedError( "Could not obtain mro for old-style classes.") bases = list(self._inferred_bases(context=context)) unmerged_mro = [[self]] + [base.mro() for base in bases] + [bases] _verify_duplicates_mro(unmerged_mro) return _c3_merge(unmerged_mro)