# 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 . # # The code in this file was originally part of logilab-common, licensed under # the same license. """ A few useful function/method decorators.""" import functools import wrapt from astroid import context as contextmod from astroid import exceptions from astroid import util @wrapt.decorator def cached(func, instance, args, kwargs): """Simple decorator to cache result of method calls without args.""" cache = getattr(instance, '__cache', None) if cache is None: instance.__cache = cache = {} try: return cache[func] except KeyError: cache[func] = result = func(*args, **kwargs) return result class cachedproperty(object): """ Provides a cached property equivalent to the stacking of @cached and @property, but more efficient. After first usage, the becomes part of the object's __dict__. Doing: del obj. 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: util.reraise(TypeError('%s must have a __name__ attribute' % wrapped)) self.wrapped = wrapped @property def __doc__(self): doc = getattr(self.wrapped, '__doc__', None) return ('%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 def path_wrapper(func): """return the given infer function wrapped to handle the path""" # TODO: switch this to wrapt after the monkey-patching is fixed (ceridwen) @functools.wraps(func) def wrapped(node, context=None, _func=func, **kwargs): """wrapper function handling context""" if context is None: context = contextmod.InferenceContext() if context.push(node): return yielded = set() generator = _func(node, context, **kwargs) try: while True: res = next(generator) # unproxy only true instance, not const, tuple, dict... if res.__class__.__name__ == 'Instance': ares = res._proxied else: ares = res if ares not in yielded: yield res yielded.add(ares) except StopIteration as error: # Explicit StopIteration to return error information, see # comment in raise_if_nothing_inferred. if len(error.args) > 0: raise StopIteration(error.args[0]) else: raise StopIteration return wrapped @wrapt.decorator def yes_if_nothing_inferred(func, instance, args, kwargs): inferred = False for node in func(*args, **kwargs): inferred = True yield node if not inferred: yield util.Uninferable @wrapt.decorator def raise_if_nothing_inferred(func, instance, args, kwargs): '''All generators wrapped with raise_if_nothing_inferred *must* explicitly raise StopIteration with information to create an appropriate structured InferenceError. ''' # TODO: Explicitly raising StopIteration in a generator will cause # a RuntimeError in Python >=3.7, as per # http://legacy.python.org/dev/peps/pep-0479/ . Before 3.7 is # released, this code will need to use one of four possible # solutions: a decorator that restores the current behavior as # described in # http://legacy.python.org/dev/peps/pep-0479/#sub-proposal-decorator-to-explicitly-request-current-behaviour # , dynamic imports or exec to generate different code for # different versions, drop support for all Python versions <3.3, # or refactoring to change how these decorators work. In any # event, after dropping support for Python <3.3 this code should # be refactored to use `yield from`. inferred = False try: generator = func(*args, **kwargs) while True: yield next(generator) inferred = True except StopIteration as error: if not inferred: if len(error.args) > 0: raise exceptions.InferenceError(**error.args[0]) else: raise exceptions.InferenceError( 'StopIteration raised without any error information.')