Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/astroid/decorators.py: 58%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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 ParamSpec, TypeVar
16from astroid import util
17from astroid.context import InferenceContext
18from astroid.exceptions import InferenceError
19from astroid.typing import InferenceResult
21_R = TypeVar("_R")
22_P = ParamSpec("_P")
25def path_wrapper(func):
26 """Return the given infer function wrapped to handle the path.
28 Used to stop inference if the node has already been looked
29 at for a given `InferenceContext` to prevent infinite recursion
30 """
32 @functools.wraps(func)
33 def wrapped(node, context: InferenceContext | None = None, _func=func) -> Generator:
34 """Wrapper function handling context."""
35 if context is None:
36 context = InferenceContext()
37 if context.push(node):
38 return
40 yielded = set()
42 for res in _func(node, context):
43 # unproxy only true instance, not const, tuple, dict...
44 if res.__class__.__name__ == "Instance":
45 ares = res._proxied
46 else:
47 ares = res
48 if ares not in yielded:
49 yield res
50 yielded.add(ares)
52 return wrapped
55def yes_if_nothing_inferred(
56 func: Callable[_P, Generator[InferenceResult]],
57) -> Callable[_P, Generator[InferenceResult]]:
58 def inner(*args: _P.args, **kwargs: _P.kwargs) -> Generator[InferenceResult]:
59 generator = func(*args, **kwargs)
61 try:
62 yield next(generator)
63 except StopIteration:
64 # generator is empty
65 yield util.Uninferable
66 return
68 yield from generator
70 return inner
73def raise_if_nothing_inferred(
74 func: Callable[_P, Generator[InferenceResult]],
75) -> Callable[_P, Generator[InferenceResult]]:
76 def inner(*args: _P.args, **kwargs: _P.kwargs) -> Generator[InferenceResult]:
77 generator = func(*args, **kwargs)
78 try:
79 yield next(generator)
80 except StopIteration as error:
81 # generator is empty
82 if error.args:
83 raise InferenceError(**error.args[0]) from error
84 raise InferenceError(
85 "StopIteration raised without any error information."
86 ) from error
87 except RecursionError as error:
88 raise InferenceError(
89 f"RecursionError raised with limit {sys.getrecursionlimit()}."
90 ) from error
92 yield from generator
94 return inner
97# Expensive decorators only used to emit Deprecation warnings.
98# If no other than the default DeprecationWarning are enabled,
99# fall back to passthrough implementations.
100if util.check_warnings_filter(): # noqa: C901
102 def deprecate_default_argument_values(
103 astroid_version: str = "3.0", **arguments: str
104 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
105 """Decorator which emits a DeprecationWarning if any arguments specified
106 are None or not passed at all.
108 Arguments should be a key-value mapping, with the key being the argument to check
109 and the value being a type annotation as string for the value of the argument.
111 To improve performance, only used when DeprecationWarnings other than
112 the default one are enabled.
113 """
114 # Helpful links
115 # Decorator for DeprecationWarning: https://stackoverflow.com/a/49802489
116 # Typing of stacked decorators: https://stackoverflow.com/a/68290080
118 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
119 """Decorator function."""
121 @functools.wraps(func)
122 def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
123 """Emit DeprecationWarnings if conditions are met."""
125 keys = list(inspect.signature(func).parameters.keys())
126 for arg, type_annotation in arguments.items():
127 try:
128 index = keys.index(arg)
129 except ValueError:
130 raise ValueError(
131 f"Can't find argument '{arg}' for '{args[0].__class__.__qualname__}'"
132 ) from None
133 # pylint: disable = too-many-boolean-expressions
134 if (
135 # Check kwargs
136 # - if found, check it's not None
137 (arg in kwargs and kwargs[arg] is None)
138 # Check args
139 # - make sure not in kwargs
140 # - len(args) needs to be long enough, if too short
141 # arg can't be in args either
142 # - args[index] should not be None
143 or (
144 arg not in kwargs
145 and (
146 index == -1
147 or len(args) <= index
148 or (len(args) > index and args[index] is None)
149 )
150 )
151 ):
152 warnings.warn(
153 f"'{arg}' will be a required argument for "
154 f"'{args[0].__class__.__qualname__}.{func.__name__}'"
155 f" in astroid {astroid_version} "
156 f"('{arg}' should be of type: '{type_annotation}')",
157 DeprecationWarning,
158 stacklevel=2,
159 )
160 return func(*args, **kwargs)
162 return wrapper
164 return deco
166 def deprecate_arguments(
167 astroid_version: str = "3.0", **arguments: str
168 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
169 """Decorator which emits a DeprecationWarning if any arguments specified
170 are passed.
172 Arguments should be a key-value mapping, with the key being the argument to check
173 and the value being a string that explains what to do instead of passing the argument.
175 To improve performance, only used when DeprecationWarnings other than
176 the default one are enabled.
177 """
179 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
180 @functools.wraps(func)
181 def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> _R:
182 keys = list(inspect.signature(func).parameters.keys())
183 for arg, note in arguments.items():
184 try:
185 index = keys.index(arg)
186 except ValueError:
187 raise ValueError(
188 f"Can't find argument '{arg}' for '{args[0].__class__.__qualname__}'"
189 ) from None
190 if arg in kwargs or len(args) > index:
191 warnings.warn(
192 f"The argument '{arg}' for "
193 f"'{args[0].__class__.__qualname__}.{func.__name__}' is deprecated "
194 f"and will be removed in astroid {astroid_version} ({note})",
195 DeprecationWarning,
196 stacklevel=2,
197 )
198 return func(*args, **kwargs)
200 return wrapper
202 return deco
204else:
206 def deprecate_default_argument_values(
207 astroid_version: str = "3.0", **arguments: str
208 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
209 """Passthrough decorator to improve performance if DeprecationWarnings are
210 disabled.
211 """
213 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
214 """Decorator function."""
215 return func
217 return deco
219 def deprecate_arguments(
220 astroid_version: str = "3.0", **arguments: str
221 ) -> Callable[[Callable[_P, _R]], Callable[_P, _R]]:
222 """Passthrough decorator to improve performance if DeprecationWarnings are
223 disabled.
224 """
226 def deco(func: Callable[_P, _R]) -> Callable[_P, _R]:
227 """Decorator function."""
228 return func
230 return deco