# copyright 2003-2013 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
# contact http://www.logilab.fr/ -- mailto:[email protected]
#
# 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/>.
"""This module contains base classes and functions for the nodes and some
inference utils.
"""
__docformat__ = "restructuredtext en"
import sys
from contextlib import contextmanager
from logilab.common.decorators import cachedproperty
from astroid.exceptions import (InferenceError, AstroidError, NotFoundError,
UnresolvableName, UseInferenceDefault)
if sys.version_info >= (3, 0):
BUILTINS = 'builtins'
else:
BUILTINS = '__builtin__'
class Proxy(object):
"""a simple proxy object"""
_proxied = None # proxied object may be set by class or by instance
def __init__(self, proxied=None):
if proxied is not None:
self._proxied = proxied
def __getattr__(self, name):
if name == '_proxied':
return getattr(self.__class__, '_proxied')
if name in self.__dict__:
return self.__dict__[name]
return getattr(self._proxied, name)
def infer(self, context=None):
yield self
# Inference ##################################################################
MISSING = object()
class InferenceContext(object):
__slots__ = ('path', 'callcontext', 'boundnode', 'infered')
def __init__(self,
path=None, callcontext=None, boundnode=None, infered=None):
if path is None:
self.path = frozenset()
else:
self.path = path
self.callcontext = callcontext
self.boundnode = boundnode
if infered is None:
self.infered = {}
else:
self.infered = infered
def push(self, key):
# This returns a NEW context with the same attributes, but a new key
# added to `path`. The intention is that it's only passed to callees
# and then destroyed; otherwise scope() may not work correctly.
# The cache will be shared, since it's the same exact dict.
if key in self.path:
# End the containing generator
raise StopIteration
return InferenceContext(
self.path.union([key]),
self.callcontext,
self.boundnode,
self.infered,
)
@contextmanager
def scope(self, callcontext=MISSING, boundnode=MISSING):
try:
orig = self.callcontext, self.boundnode
if callcontext is not MISSING:
self.callcontext = callcontext
if boundnode is not MISSING:
self.boundnode = boundnode
yield
finally:
self.callcontext, self.boundnode = orig
def cache_generator(self, key, generator):
results = []
for result in generator:
results.append(result)
yield result
self.infered[key] = tuple(results)
return
def _infer_stmts(stmts, context, frame=None, lookupname=None):
"""return an iterator on statements inferred by each statement in <stmts>
"""
stmt = None
infered = False
if context is None:
context = InferenceContext()
for stmt in stmts:
if stmt is YES:
yield stmt
infered = True
continue
kw = {}
infered_name = stmt._infer_name(frame, lookupname)
if infered_name is not None:
# only returns not None if .infer() accepts a lookupname kwarg
kw['lookupname'] = infered_name
try:
for infered in stmt.infer(context, **kw):
yield infered
infered = True
except UnresolvableName:
continue
except InferenceError:
yield YES
infered = True
if not infered:
raise InferenceError(str(stmt))
# special inference objects (e.g. may be returned as nodes by .infer()) #######
class _Yes(object):
"""a yes object"""
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('__'):
# to avoid inspection pb
return super(_Yes, self).__getattribute__(name)
return self
def __call__(self, *args, **kwargs):
return self
YES = _Yes()
class Instance(Proxy):
"""a special node representing a class instance"""
def getattr(self, name, context=None, lookupclass=True):
try:
values = self._proxied.instance_attr(name, context)
except NotFoundError:
if name == '__class__':
return [self._proxied]
if lookupclass:
# class attributes not available through the instance
# 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)
# since we've no context information, return matching class members as
# well
if lookupclass:
try:
return values + self._proxied.getattr(name, context)
except NotFoundError:
pass
return values
def igetattr(self, name, context=None):
"""inferred getattr"""
if not context:
context = InferenceContext()
try:
# avoid recursively inferring the same attr on the same class
new_context = context.push((self._proxied, name))
# XXX frame should be self._proxied, or not ?
get_attr = self.getattr(name, new_context, lookupclass=False)
return _infer_stmts(
self._wrap_attr(get_attr, new_context),
new_context,
frame=self,
)
except 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)
def _wrap_attr(self, attrs, context=None):
"""wrap bound methods of attrs in a InstanceMethod proxies"""
for attr in attrs:
if isinstance(attr, UnboundMethod):
if BUILTINS + '.property' in attr.decoratornames():
for infered in attr.infer_call_result(self, context):
yield infered
else:
yield BoundMethod(attr, self)
else:
yield attr
def infer_call_result(self, caller, context=None):
"""infer what a class instance is returning when called"""
infered = False
for node in self._proxied.igetattr('__call__', context):
if node is YES:
continue
for res in node.infer_call_result(caller, context):
infered = True
yield res
if not infered:
raise InferenceError()
def __repr__(self):
return '<Instance of %s.%s at 0x%s>' % (self._proxied.root().name,
self._proxied.name,
id(self))
def __str__(self):
return 'Instance of %s.%s' % (self._proxied.root().name,
self._proxied.name)
def callable(self):
try:
self._proxied.getattr('__call__')
return True
except NotFoundError:
return False
def pytype(self):
return self._proxied.qname()
def display_type(self):
return 'Instance of'
class UnboundMethod(Proxy):
"""a special node representing a method not bound to an instance"""
def __repr__(self):
frame = self._proxied.parent.frame()
return '<%s %s of %s at 0x%s' % (self.__class__.__name__,
self._proxied.name,
frame.qname(), id(self))
def is_bound(self):
return False
def getattr(self, name, context=None):
if name == 'im_func':
return [self._proxied]
return super(UnboundMethod, self).getattr(name, context)
def igetattr(self, name, context=None):
if name == 'im_func':
return iter((self._proxied,))
return super(UnboundMethod, self).igetattr(name, context)
def infer_call_result(self, caller, context):
# If we're unbound method __new__ of builtin object, the result is an
# instance of the class given as first argument.
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 self._proxied.infer_call_result(caller, context)
class BoundMethod(UnboundMethod):
"""a special node representing a method bound to an instance"""
def __init__(self, proxy, bound):
UnboundMethod.__init__(self, proxy)
self.bound = bound
def is_bound(self):
return True
def infer_call_result(self, caller, context):
with context.scope(boundnode=self.bound):
for infered in self._proxied.infer_call_result(caller, context):
yield infered
class Generator(Instance):
"""a special node representing a generator.
Proxied class is set once for all in raw_building.
"""
def callable(self):
return False
def pytype(self):
return '%s.generator' % BUILTINS
def display_type(self):
return 'Generator'
def __repr__(self):
return '<Generator(%s) l.%s at 0x%s>' % (self._proxied.name, self.lineno, id(self))
def __str__(self):
return 'Generator(%s)' % (self._proxied.name)
# decorators ##################################################################
def path_wrapper(func):
"""return the given infer function wrapped to handle the path"""
def wrapped(node, context=None, _func=func, **kwargs):
"""wrapper function handling context"""
if context is None:
context = InferenceContext()
context = context.push((node, kwargs.get('lookupname')))
yielded = set()
for res in _func(node, context, **kwargs):
# unproxy only true instance, not const, tuple, dict...
if res.__class__ is Instance:
ares = res._proxied
else:
ares = res
if not ares in yielded:
yield res
yielded.add(ares)
return wrapped
def yes_if_nothing_infered(func):
def wrapper(*args, **kwargs):
infered = False
for node in func(*args, **kwargs):
infered = True
yield node
if not infered:
yield YES
return wrapper
def raise_if_nothing_infered(func):
def wrapper(*args, **kwargs):
infered = False
for node in func(*args, **kwargs):
infered = True
yield node
if not infered:
raise InferenceError()
return wrapper
# Node ######################################################################
class NodeNG(object):
"""Base Class for all Astroid node classes.
It represents a node of the new abstract syntax tree.
"""
is_statement = False
optional_assign = False # True for For (and for Comprehension if py <3.0)
is_function = False # True for Function nodes
# attributes below are set by the builder module or by raw factories
lineno = None
fromlineno = None
tolineno = None
col_offset = None
# parent node in the tree
parent = None
# attributes containing child node(s) redefined in most concrete classes:
_astroid_fields = ()
# instance specific inference function infer(node, context)
_explicit_inference = None
def infer(self, context=None, **kwargs):
"""main interface to the interface system, return a generator on infered
values.
If the instance has some explicit inference function set, it will be
called instead of the default interface.
"""
if self._explicit_inference is not None:
# explicit_inference is not bound, give it self explicitly
try:
return self._explicit_inference(self, context, **kwargs)
except UseInferenceDefault:
pass
if not context:
return self._infer(context, **kwargs)
key = (self, kwargs.get('lookupname'), context.callcontext, context.boundnode)
if key in context.infered:
return iter(context.infered[key])
return context.cache_generator(key, self._infer(context, **kwargs))
def _repr_name(self):
"""return self.name or self.attrname or '' for nice representation"""
return getattr(self, 'name', getattr(self, 'attrname', ''))
def __str__(self):
return '%s(%s)' % (self.__class__.__name__, self._repr_name())
def __repr__(self):
return '<%s(%s) l.%s [%s] at 0x%x>' % (self.__class__.__name__,
self._repr_name(),
self.fromlineno,
self.root().name,
id(self))
def accept(self, visitor):
func = getattr(visitor, "visit_" + self.__class__.__name__.lower())
return func(self)
def get_children(self):
for field in self._astroid_fields:
attr = getattr(self, field)
if attr is None:
continue
if isinstance(attr, (list, tuple)):
for elt in attr:
yield elt
else:
yield attr
def last_child(self):
"""an optimized version of list(get_children())[-1]"""
for field in self._astroid_fields[::-1]:
attr = getattr(self, field)
if not attr: # None or empty listy / tuple
continue
if attr.__class__ in (list, tuple):
return attr[-1]
else:
return attr
return None
def parent_of(self, node):
"""return true if i'm a parent of the given node"""
parent = node.parent
while parent is not None:
if self is parent:
return True
parent = parent.parent
return False
def statement(self):
"""return the first parent node marked as statement node"""
if self.is_statement:
return self
return self.parent.statement()
def frame(self):
"""return the first parent frame node (i.e. Module, Function or Class)
"""
return self.parent.frame()
def scope(self):
"""return the first node defining a new scope (i.e. Module, Function,
Class, Lambda but also GenExpr)
"""
return self.parent.scope()
def root(self):
"""return the root node of the tree, (i.e. a Module)"""
if self.parent:
return self.parent.root()
return self
def child_sequence(self, child):
"""search for the right sequence where the child lies in"""
for field in self._astroid_fields:
node_or_sequence = getattr(self, field)
if node_or_sequence is child:
return [node_or_sequence]
# /!\ compiler.ast Nodes have an __iter__ walking over child nodes
if isinstance(node_or_sequence, (tuple, list)) and child in node_or_sequence:
return node_or_sequence
else:
msg = 'Could not find %s in %s\'s children'
raise AstroidError(msg % (repr(child), repr(self)))
def locate_child(self, child):
"""return a 2-uple (child attribute name, sequence or node)"""
for field in self._astroid_fields:
node_or_sequence = getattr(self, field)
# /!\ compiler.ast Nodes have an __iter__ walking over child nodes
if child is node_or_sequence:
return field, child
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)))
# FIXME : should we merge child_sequence and locate_child ? locate_child
# is only used in are_exclusive, child_sequence one time in pylint.
def next_sibling(self):
"""return the next sibling statement"""
return self.parent.next_sibling()
def previous_sibling(self):
"""return the previous sibling statement"""
return self.parent.previous_sibling()
def nearest(self, nodes):
"""return the node which is the nearest before this one in the
given list of nodes
"""
myroot = self.root()
mylineno = self.fromlineno
nearest = None, 0
for node in nodes:
assert node.root() is myroot, \
'nodes %s and %s are not from the same module' % (self, node)
lineno = node.fromlineno
if node.fromlineno > mylineno:
break
if lineno > nearest[1]:
nearest = node, lineno
# FIXME: raise an exception if nearest is None ?
return nearest[0]
# these are lazy because they're relatively expensive to compute for every
# single node, and they rarely get looked at
@cachedproperty
def fromlineno(self):
if self.lineno is None:
return self._fixed_source_line()
else:
return self.lineno
@cachedproperty
def tolineno(self):
if not self._astroid_fields:
# can't have children
lastchild = None
else:
lastchild = self.last_child()
if lastchild is None:
return self.fromlineno
else:
return lastchild.tolineno
# TODO / FIXME:
assert self.fromlineno is not None, self
assert self.tolineno is not None, self
def _fixed_source_line(self):
"""return the line number where the given node appears
we need this method since not all nodes have the lineno attribute
correctly set...
"""
line = self.lineno
_node = self
try:
while line is None:
_node = next(_node.get_children())
line = _node.lineno
except StopIteration:
_node = self.parent
while _node and line is None:
line = _node.lineno
_node = _node.parent
return line
def block_range(self, lineno):
"""handle block line numbers range for non block opening statements
"""
return lineno, self.tolineno
def set_local(self, name, stmt):
"""delegate to a scoped parent handling a locals dictionary"""
self.parent.set_local(name, stmt)
def nodes_of_class(self, klass, skip_klass=None):
"""return an iterator on nodes which are instance of the given class(es)
klass may be a class object or a tuple of class objects
"""
if isinstance(self, klass):
yield self
for child_node in self.get_children():
if skip_klass is not None and isinstance(child_node, skip_klass):
continue
for matching in child_node.nodes_of_class(klass, skip_klass):
yield matching
def _infer_name(self, frame, name):
# overridden for From, Import, Global, TryExcept and Arguments
return None
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__)
def infered(self):
'''return list of infered values for a more simple inference usage'''
return list(self.infer())
def instanciate_class(self):
"""instanciate a node if it is a Class node, else return self"""
return self
def has_base(self, node):
return False
def callable(self):
return False
def eq(self, value):
return False
def as_string(self):
from astroid.as_string import to_code
return to_code(self)
def repr_tree(self, ids=False):
from astroid.as_string import dump
return dump(self)
class Statement(NodeNG):
"""Statement node adding a few attributes"""
is_statement = True
def next_sibling(self):
"""return the next sibling statement"""
stmts = self.parent.child_sequence(self)
index = stmts.index(self)
try:
return stmts[index +1]
except IndexError:
pass
def previous_sibling(self):
"""return the previous sibling statement"""
stmts = self.parent.child_sequence(self)
index = stmts.index(self)
if index >= 1:
return stmts[index -1]