decorators.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
  2. # For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
  3. # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
  4. """A few useful function/method decorators."""
  5. from __future__ import annotations
  6. import functools
  7. import inspect
  8. import sys
  9. import warnings
  10. from collections.abc import Callable, Generator
  11. from typing import TypeVar
  12. import wrapt
  13. from astroid import _cache, util
  14. from astroid.context import InferenceContext
  15. from astroid.exceptions import InferenceError
  16. if sys.version_info >= (3, 10):
  17. from typing import ParamSpec
  18. else:
  19. from typing_extensions import ParamSpec
  20. _R = TypeVar("_R")
  21. _P = ParamSpec("_P")
  22. @wrapt.decorator
  23. def cached(func, instance, args, kwargs):
  24. """Simple decorator to cache result of method calls without args."""
  25. cache = getattr(instance, "__cache", None)
  26. if cache is None:
  27. instance.__cache = cache = {}
  28. _cache.CACHE_MANAGER.add_dict_cache(cache)
  29. try:
  30. return cache[func]
  31. except KeyError:
  32. cache[func] = result = func(*args, **kwargs)
  33. return result
  34. # TODO: Remove when support for 3.7 is dropped
  35. # TODO: astroid 3.0 -> move class behind sys.version_info < (3, 8) guard
  36. class cachedproperty:
  37. """Provides a cached property equivalent to the stacking of
  38. @cached and @property, but more efficient.
  39. After first usage, the <property_name> becomes part of the object's
  40. __dict__. Doing:
  41. del obj.<property_name> empties the cache.
  42. Idea taken from the pyramid_ framework and the mercurial_ project.
  43. .. _pyramid: http://pypi.python.org/pypi/pyramid
  44. .. _mercurial: http://pypi.python.org/pypi/Mercurial
  45. """
  46. __slots__ = ("wrapped",)
  47. def __init__(self, wrapped):
  48. if sys.version_info >= (3, 8):
  49. warnings.warn(
  50. "cachedproperty has been deprecated and will be removed in astroid 3.0 for Python 3.8+. "
  51. "Use functools.cached_property instead.",
  52. DeprecationWarning,
  53. stacklevel=2,
  54. )
  55. try:
  56. wrapped.__name__
  57. except AttributeError as exc:
  58. raise TypeError(f"{wrapped} must have a __name__ attribute") from exc
  59. self.wrapped = wrapped
  60. @property
  61. def __doc__(self):
  62. doc = getattr(self.wrapped, "__doc__", None)
  63. return "<wrapped by the cachedproperty decorator>%s" % (
  64. "\n%s" % doc if doc else ""
  65. )
  66. def __get__(self, inst, objtype=None):
  67. if inst is None:
  68. return self
  69. val = self.wrapped(inst)
  70. setattr(inst, self.wrapped.__name__, val)
  71. return val
  72. def path_wrapper(func):
  73. """Return the given infer function wrapped to handle the path.
  74. Used to stop inference if the node has already been looked
  75. at for a given `InferenceContext` to prevent infinite recursion
  76. """
  77. @functools.wraps(func)
  78. def wrapped(
  79. node, context: InferenceContext | None = None, _func=func, **kwargs
  80. ) -> Generator:
  81. """Wrapper function handling context."""
  82. if context is None:
  83. context = InferenceContext()
  84. if context.push(node):
  85. return
  86. yielded = set()
  87. for res in _func(node, context, **kwargs):
  88. # unproxy only true instance, not const, tuple, dict...
  89. if res.__class__.__name__ == "Instance":
  90. ares = res._proxied
  91. else:
  92. ares = res
  93. if ares not in yielded:
  94. yield res
  95. yielded.add(ares)
  96. return wrapped
  97. @wrapt.decorator
  98. def yes_if_nothing_inferred(func, instance, args, kwargs):
  99. generator = func(*args, **kwargs)
  100. try:
  101. yield next(generator)
  102. except StopIteration:
  103. # generator is empty
  104. yield util.Uninferable
  105. return
  106. yield from generator
  107. @wrapt.decorator
  108. def raise_if_nothing_inferred(func, instance, args, kwargs):
  109. generator = func(*args, **kwargs)
  110. try:
  111. yield next(generator)
  112. except StopIteration as error:
  113. # generator is empty
  114. if error.args:
  115. # pylint: disable=not-a-mapping
  116. raise InferenceError(**error.args[0]) from error
  117. raise InferenceError(
  118. "StopIteration raised without any error information."
  119. ) from error
  120. except RecursionError as error:
  121. raise InferenceError(
  122. f"RecursionError raised with limit {sys.getrecursionlimit()}."
  123. ) from error
  124. yield from generator
  125. # Expensive decorators only used to emit Deprecation warnings.
  126. # If no other than the default DeprecationWarning are enabled,
  127. # fall back to passthrough implementations.
  128. if util.check_warnings_filter(): # noqa: C901
  129. def deprecate_default_argument_values(
  130. astroid_version: str = "3.0", **arguments: str
  131. ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
  132. """Decorator which emits a DeprecationWarning if any arguments specified
  133. are None or not passed at all.
  134. Arguments should be a key-value mapping, with the key being the argument to check
  135. and the value being a type annotation as string for the value of the argument.
  136. To improve performance, only used when DeprecationWarnings other than
  137. the default one are enabled.
  138. """
  139. # Helpful links
  140. # Decorator for DeprecationWarning: https://stackoverflow.com/a/49802489
  141. # Typing of stacked decorators: https://stackoverflow.com/a/68290080
  142. def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
  143. """Decorator function."""
  144. @functools.wraps(func)
  145. def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
  146. """Emit DeprecationWarnings if conditions are met."""
  147. keys = list(inspect.signature(func).parameters.keys())
  148. for arg, type_annotation in arguments.items():
  149. try:
  150. index = keys.index(arg)
  151. except ValueError:
  152. raise ValueError(
  153. f"Can't find argument '{arg}' for '{args[0].__class__.__qualname__}'"
  154. ) from None
  155. if (
  156. # Check kwargs
  157. # - if found, check it's not None
  158. (arg in kwargs and kwargs[arg] is None)
  159. # Check args
  160. # - make sure not in kwargs
  161. # - len(args) needs to be long enough, if too short
  162. # arg can't be in args either
  163. # - args[index] should not be None
  164. or arg not in kwargs
  165. and (
  166. index == -1
  167. or len(args) <= index
  168. or (len(args) > index and args[index] is None)
  169. )
  170. ):
  171. warnings.warn(
  172. f"'{arg}' will be a required argument for "
  173. f"'{args[0].__class__.__qualname__}.{func.__name__}'"
  174. f" in astroid {astroid_version} "
  175. f"('{arg}' should be of type: '{type_annotation}')",
  176. DeprecationWarning,
  177. stacklevel=2,
  178. )
  179. return func(*args, **kwargs)
  180. return wrapper
  181. return deco
  182. def deprecate_arguments(
  183. astroid_version: str = "3.0", **arguments: str
  184. ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
  185. """Decorator which emits a DeprecationWarning if any arguments specified
  186. are passed.
  187. Arguments should be a key-value mapping, with the key being the argument to check
  188. and the value being a string that explains what to do instead of passing the argument.
  189. To improve performance, only used when DeprecationWarnings other than
  190. the default one are enabled.
  191. """
  192. def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
  193. @functools.wraps(func)
  194. def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
  195. keys = list(inspect.signature(func).parameters.keys())
  196. for arg, note in arguments.items():
  197. try:
  198. index = keys.index(arg)
  199. except ValueError:
  200. raise ValueError(
  201. f"Can't find argument '{arg}' for '{args[0].__class__.__qualname__}'"
  202. ) from None
  203. if arg in kwargs or len(args) > index:
  204. warnings.warn(
  205. f"The argument '{arg}' for "
  206. f"'{args[0].__class__.__qualname__}.{func.__name__}' is deprecated "
  207. f"and will be removed in astroid {astroid_version} ({note})",
  208. DeprecationWarning,
  209. stacklevel=2,
  210. )
  211. return func(*args, **kwargs)
  212. return wrapper
  213. return deco
  214. else:
  215. def deprecate_default_argument_values(
  216. astroid_version: str = "3.0", **arguments: str
  217. ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
  218. """Passthrough decorator to improve performance if DeprecationWarnings are
  219. disabled.
  220. """
  221. def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
  222. """Decorator function."""
  223. return func
  224. return deco
  225. def deprecate_arguments(
  226. astroid_version: str = "3.0", **arguments: str
  227. ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
  228. """Passthrough decorator to improve performance if DeprecationWarnings are
  229. disabled.
  230. """
  231. def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
  232. """Decorator function."""
  233. return func
  234. return deco