Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/astroid/decorators.py: 67%
95 statements
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:53 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2023-06-07 06:53 +0000
1# Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html
2# For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE
3# Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt
5"""A few useful function/method decorators."""
7from __future__ import annotations
9import functools
10import inspect
11import sys
12import warnings
13from collections.abc import Callable, Generator
14from typing import TypeVar
16from astroid import util
17from astroid.context import InferenceContext
18from astroid.exceptions import InferenceError
19from astroid.typing import InferenceResult
21if sys.version_info >= (3, 10):
22 from typing import ParamSpec
23else:
24 from typing_extensions import ParamSpec
26_R = TypeVar("_R")
27_P = ParamSpec("_P")
30def path_wrapper(func):
31 """Return the given infer function wrapped to handle the path.
33 Used to stop inference if the node has already been looked
34 at for a given `InferenceContext` to prevent infinite recursion
35 """
37 @functools.wraps(func)
38 def wrapped(
39 node, context: InferenceContext | None = None, _func=func, **kwargs
40 ) -> Generator:
41 """Wrapper function handling context."""
42 if context is None:
43 context = InferenceContext()
44 if context.push(node):
45 return
47 yielded = set()
49 for res in _func(node, context, **kwargs):
50 # unproxy only true instance, not const, tuple, dict...
51 if res.__class__.__name__ == "Instance":
52 ares = res._proxied
53 else:
54 ares = res
55 if ares not in yielded:
56 yield res
57 yielded.add(ares)
59 return wrapped
62def yes_if_nothing_inferred(
63 func: Callable[_P, Generator[InferenceResult, None, None]]
64) -> Callable[_P, Generator[InferenceResult, None, None]]:
65 def inner(
66 *args: _P.args, **kwargs: _P.kwargs
67 ) -> Generator[InferenceResult, None, None]:
68 generator = func(*args, **kwargs)
70 try:
71 yield next(generator)
72 except StopIteration:
73 # generator is empty
74 yield util.Uninferable
75 return
77 yield from generator
79 return inner
82def raise_if_nothing_inferred(
83 func: Callable[_P, Generator[InferenceResult, None, None]],
84) -> Callable[_P, Generator[InferenceResult, None, None]]:
85 def inner(
86 *args: _P.args, **kwargs: _P.kwargs
87 ) -> Generator[InferenceResult, None, None]:
88 generator = func(*args, **kwargs)
89 try:
90 yield next(generator)
91 except StopIteration as error:
92 # generator is empty
93 if error.args:
94 raise InferenceError(**error.args[0]) from error
95 raise InferenceError(
96 "StopIteration raised without any error information."
97 ) from error
98 except RecursionError as error:
99 raise InferenceError(
100 f"RecursionError raised with limit {sys.getrecursionlimit()}."
101 ) from error
103 yield from generator
105 return inner
108# Expensive decorators only used to emit Deprecation warnings.
109# If no other than the default DeprecationWarning are enabled,
110# fall back to passthrough implementations.
111if util.check_warnings_filter(): # noqa: C901
113 def deprecate_default_argument_values(
114 astroid_version: str = "3.0", **arguments: str
115 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
116 """Decorator which emits a DeprecationWarning if any arguments specified
117 are None or not passed at all.
119 Arguments should be a key-value mapping, with the key being the argument to check
120 and the value being a type annotation as string for the value of the argument.
122 To improve performance, only used when DeprecationWarnings other than
123 the default one are enabled.
124 """
125 # Helpful links
126 # Decorator for DeprecationWarning: https://stackoverflow.com/a/49802489
127 # Typing of stacked decorators: https://stackoverflow.com/a/68290080
129 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
130 """Decorator function."""
132 @functools.wraps(func)
133 def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
134 """Emit DeprecationWarnings if conditions are met."""
136 keys = list(inspect.signature(func).parameters.keys())
137 for arg, type_annotation in arguments.items():
138 try:
139 index = keys.index(arg)
140 except ValueError:
141 raise ValueError(
142 f"Can't find argument '{arg}' for '{args[0].__class__.__qualname__}'"
143 ) from None
144 if (
145 # Check kwargs
146 # - if found, check it's not None
147 (arg in kwargs and kwargs[arg] is None)
148 # Check args
149 # - make sure not in kwargs
150 # - len(args) needs to be long enough, if too short
151 # arg can't be in args either
152 # - args[index] should not be None
153 or arg not in kwargs
154 and (
155 index == -1
156 or len(args) <= index
157 or (len(args) > index and args[index] is None)
158 )
159 ):
160 warnings.warn(
161 f"'{arg}' will be a required argument for "
162 f"'{args[0].__class__.__qualname__}.{func.__name__}'"
163 f" in astroid {astroid_version} "
164 f"('{arg}' should be of type: '{type_annotation}')",
165 DeprecationWarning,
166 stacklevel=2,
167 )
168 return func(*args, **kwargs)
170 return wrapper
172 return deco
174 def deprecate_arguments(
175 astroid_version: str = "3.0", **arguments: str
176 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
177 """Decorator which emits a DeprecationWarning if any arguments specified
178 are passed.
180 Arguments should be a key-value mapping, with the key being the argument to check
181 and the value being a string that explains what to do instead of passing the argument.
183 To improve performance, only used when DeprecationWarnings other than
184 the default one are enabled.
185 """
187 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
188 @functools.wraps(func)
189 def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
190 keys = list(inspect.signature(func).parameters.keys())
191 for arg, note in arguments.items():
192 try:
193 index = keys.index(arg)
194 except ValueError:
195 raise ValueError(
196 f"Can't find argument '{arg}' for '{args[0].__class__.__qualname__}'"
197 ) from None
198 if arg in kwargs or len(args) > index:
199 warnings.warn(
200 f"The argument '{arg}' for "
201 f"'{args[0].__class__.__qualname__}.{func.__name__}' is deprecated "
202 f"and will be removed in astroid {astroid_version} ({note})",
203 DeprecationWarning,
204 stacklevel=2,
205 )
206 return func(*args, **kwargs)
208 return wrapper
210 return deco
212else:
214 def deprecate_default_argument_values(
215 astroid_version: str = "3.0", **arguments: str
216 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
217 """Passthrough decorator to improve performance if DeprecationWarnings are
218 disabled.
219 """
221 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
222 """Decorator function."""
223 return func
225 return deco
227 def deprecate_arguments(
228 astroid_version: str = "3.0", **arguments: str
229 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
230 """Passthrough decorator to improve performance if DeprecationWarnings are
231 disabled.
232 """
234 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
235 """Decorator function."""
236 return func
238 return deco