1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
# Copyright (c) 2015 Florian Bruhin <me@the-compiler.org>
# Copyright (c) 2015-2016 Cara Vinson <ceridwenv@gmail.com>
# Copyright (c) 2015-2016 Claudiu Popa <pcmanticore@gmail.com>
# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
# For details: https://github.com/PyCQA/astroid/blob/master/COPYING.LESSER
""" A few useful function/method decorators."""
import functools
import wrapt
from astroid import context as contextmod
from astroid import exceptions
from astroid.interpreter import runtimeabc
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 <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:
util.reraise(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
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 isinstance(res, runtimeabc.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.')
|