| Viewing file:  decorators.py (9.85 KB)      -rw-r--r-- Select action/file-type:
 
  (+) |  (+) |  (+) | Code (+) | Session (+) |  (+) | SDB (+) |  (+) |  (+) |  (+) |  (+) |  (+) | 
 
# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html# For details: https://github.com/PyCQA/astroid/blob/main/LICENSE
 # Copyright (c) https://github.com/PyCQA/astroid/blob/main/CONTRIBUTORS.txt
 
 """A few useful function/method decorators."""
 
 from __future__ import annotations
 
 import functools
 import inspect
 import sys
 import warnings
 from collections.abc import Callable, Generator
 from typing import TypeVar
 
 import wrapt
 
 from astroid import _cache, util
 from astroid.context import InferenceContext
 from astroid.exceptions import InferenceError
 
 if sys.version_info >= (3, 10):
 from typing import ParamSpec
 else:
 from typing_extensions import ParamSpec
 
 _R = TypeVar("_R")
 _P = ParamSpec("_P")
 
 
 @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 = {}
 _cache.CACHE_MANAGER.add_dict_cache(cache)
 try:
 return cache[func]
 except KeyError:
 cache[func] = result = func(*args, **kwargs)
 return result
 
 
 # TODO: Remove when support for 3.7 is dropped
 # TODO: astroid 3.0 -> move class behind sys.version_info < (3, 8) guard
 class cachedproperty:
 """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):
 if sys.version_info >= (3, 8):
 warnings.warn(
 "cachedproperty has been deprecated and will be removed in astroid 3.0 for Python 3.8+. "
 "Use functools.cached_property instead.",
 DeprecationWarning,
 stacklevel=2,
 )
 try:
 wrapped.__name__
 except AttributeError as exc:
 raise TypeError(f"{wrapped} must have a __name__ attribute") from exc
 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.
 
 Used to stop inference if the node has already been looked
 at for a given `InferenceContext` to prevent infinite recursion
 """
 
 @functools.wraps(func)
 def wrapped(
 node, context: InferenceContext | None = None, _func=func, **kwargs
 ) -> Generator:
 """Wrapper function handling context."""
 if context is None:
 context = InferenceContext()
 if context.push(node):
 return
 
 yielded = set()
 
 for res in _func(node, context, **kwargs):
 # 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)
 
 return wrapped
 
 
 @wrapt.decorator
 def yes_if_nothing_inferred(func, instance, args, kwargs):
 generator = func(*args, **kwargs)
 
 try:
 yield next(generator)
 except StopIteration:
 # generator is empty
 yield util.Uninferable
 return
 
 yield from generator
 
 
 @wrapt.decorator
 def raise_if_nothing_inferred(func, instance, args, kwargs):
 generator = func(*args, **kwargs)
 try:
 yield next(generator)
 except StopIteration as error:
 # generator is empty
 if error.args:
 # pylint: disable=not-a-mapping
 raise InferenceError(**error.args[0]) from error
 raise InferenceError(
 "StopIteration raised without any error information."
 ) from error
 except RecursionError as error:
 raise InferenceError(
 f"RecursionError raised with limit {sys.getrecursionlimit()}."
 ) from error
 
 yield from generator
 
 
 # Expensive decorators only used to emit Deprecation warnings.
 # If no other than the default DeprecationWarning are enabled,
 # fall back to passthrough implementations.
 if util.check_warnings_filter():  # noqa: C901
 
 def deprecate_default_argument_values(
 astroid_version: str = "3.0", **arguments: str
 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
 """Decorator which emits a DeprecationWarning if any arguments specified
 are None or not passed at all.
 
 Arguments should be a key-value mapping, with the key being the argument to check
 and the value being a type annotation as string for the value of the argument.
 
 To improve performance, only used when DeprecationWarnings other than
 the default one are enabled.
 """
 # Helpful links
 # Decorator for DeprecationWarning: https://stackoverflow.com/a/49802489
 # Typing of stacked decorators: https://stackoverflow.com/a/68290080
 
 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
 """Decorator function."""
 
 @functools.wraps(func)
 def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
 """Emit DeprecationWarnings if conditions are met."""
 
 keys = list(inspect.signature(func).parameters.keys())
 for arg, type_annotation in arguments.items():
 try:
 index = keys.index(arg)
 except ValueError:
 raise ValueError(
 f"Can't find argument '{arg}' for '{args[0].__class__.__qualname__}'"
 ) from None
 if (
 # Check kwargs
 # - if found, check it's not None
 (arg in kwargs and kwargs[arg] is None)
 # Check args
 # - make sure not in kwargs
 # - len(args) needs to be long enough, if too short
 #   arg can't be in args either
 # - args[index] should not be None
 or arg not in kwargs
 and (
 index == -1
 or len(args) <= index
 or (len(args) > index and args[index] is None)
 )
 ):
 warnings.warn(
 f"'{arg}' will be a required argument for "
 f"'{args[0].__class__.__qualname__}.{func.__name__}'"
 f" in astroid {astroid_version} "
 f"('{arg}' should be of type: '{type_annotation}')",
 DeprecationWarning,
 stacklevel=2,
 )
 return func(*args, **kwargs)
 
 return wrapper
 
 return deco
 
 def deprecate_arguments(
 astroid_version: str = "3.0", **arguments: str
 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
 """Decorator which emits a DeprecationWarning if any arguments specified
 are passed.
 
 Arguments should be a key-value mapping, with the key being the argument to check
 and the value being a string that explains what to do instead of passing the argument.
 
 To improve performance, only used when DeprecationWarnings other than
 the default one are enabled.
 """
 
 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
 @functools.wraps(func)
 def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
 keys = list(inspect.signature(func).parameters.keys())
 for arg, note in arguments.items():
 try:
 index = keys.index(arg)
 except ValueError:
 raise ValueError(
 f"Can't find argument '{arg}' for '{args[0].__class__.__qualname__}'"
 ) from None
 if arg in kwargs or len(args) > index:
 warnings.warn(
 f"The argument '{arg}' for "
 f"'{args[0].__class__.__qualname__}.{func.__name__}' is deprecated "
 f"and will be removed in astroid {astroid_version} ({note})",
 DeprecationWarning,
 stacklevel=2,
 )
 return func(*args, **kwargs)
 
 return wrapper
 
 return deco
 
 else:
 
 def deprecate_default_argument_values(
 astroid_version: str = "3.0", **arguments: str
 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
 """Passthrough decorator to improve performance if DeprecationWarnings are
 disabled.
 """
 
 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
 """Decorator function."""
 return func
 
 return deco
 
 def deprecate_arguments(
 astroid_version: str = "3.0", **arguments: str
 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
 """Passthrough decorator to improve performance if DeprecationWarnings are
 disabled.
 """
 
 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
 """Decorator function."""
 return func
 
 return deco
 
 |