diff options
Diffstat (limited to 'astroid/bases.py')
| -rw-r--r-- | astroid/bases.py | 665 |
1 files changed, 665 insertions, 0 deletions
diff --git a/astroid/bases.py b/astroid/bases.py new file mode 100644 index 00000000..f1f4cc4b --- /dev/null +++ b/astroid/bases.py @@ -0,0 +1,665 @@ +# 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/>. +"""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] |
