Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/astroid/arguments.py: 82%
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
5from __future__ import annotations
7from astroid import nodes
8from astroid.bases import Instance
9from astroid.context import CallContext, InferenceContext
10from astroid.exceptions import InferenceError, NoDefault
11from astroid.typing import InferenceResult
12from astroid.util import Uninferable, UninferableBase, safe_infer
15class CallSite:
16 """Class for understanding arguments passed into a call site.
18 It needs a call context, which contains the arguments and the
19 keyword arguments that were passed into a given call site.
20 In order to infer what an argument represents, call :meth:`infer_argument`
21 with the corresponding function node and the argument name.
23 :param callcontext:
24 An instance of :class:`astroid.context.CallContext`, that holds
25 the arguments for the call site.
26 :param argument_context_map:
27 Additional contexts per node, passed in from :attr:`astroid.context.Context.extra_context`
28 :param context:
29 An instance of :class:`astroid.context.Context`.
30 """
32 def __init__(
33 self,
34 callcontext: CallContext,
35 argument_context_map=None,
36 context: InferenceContext | None = None,
37 ):
38 if argument_context_map is None:
39 argument_context_map = {}
40 self.argument_context_map = argument_context_map
41 args = callcontext.args
42 keywords = callcontext.keywords
43 self.duplicated_keywords: set[str] = set()
44 self._unpacked_args = self._unpack_args(args, context=context)
45 self._unpacked_kwargs = self._unpack_keywords(keywords, context=context)
47 self.positional_arguments = [
48 arg for arg in self._unpacked_args if not isinstance(arg, UninferableBase)
49 ]
50 self.keyword_arguments = {
51 key: value
52 for key, value in self._unpacked_kwargs.items()
53 if not isinstance(value, UninferableBase)
54 }
56 @classmethod
57 def from_call(cls, call_node, context: InferenceContext | None = None):
58 """Get a CallSite object from the given Call node.
60 context will be used to force a single inference path.
61 """
63 # Determine the callcontext from the given `context` object if any.
64 context = context or InferenceContext()
65 callcontext = CallContext(call_node.args, call_node.keywords)
66 return cls(callcontext, context=context)
68 def has_invalid_arguments(self):
69 """Check if in the current CallSite were passed *invalid* arguments.
71 This can mean multiple things. For instance, if an unpacking
72 of an invalid object was passed, then this method will return True.
73 Other cases can be when the arguments can't be inferred by astroid,
74 for example, by passing objects which aren't known statically.
75 """
76 return len(self.positional_arguments) != len(self._unpacked_args)
78 def has_invalid_keywords(self) -> bool:
79 """Check if in the current CallSite were passed *invalid* keyword arguments.
81 For instance, unpacking a dictionary with integer keys is invalid
82 (**{1:2}), because the keys must be strings, which will make this
83 method to return True. Other cases where this might return True if
84 objects which can't be inferred were passed.
85 """
86 return len(self.keyword_arguments) != len(self._unpacked_kwargs)
88 def _unpack_keywords(
89 self,
90 keywords: list[tuple[str | None, nodes.NodeNG]],
91 context: InferenceContext | None = None,
92 ):
93 values: dict[str | None, InferenceResult] = {}
94 context = context or InferenceContext()
95 context.extra_context = self.argument_context_map
96 for name, value in keywords:
97 if name is None:
98 # Then it's an unpacking operation (**)
99 inferred = safe_infer(value, context=context)
100 if not isinstance(inferred, nodes.Dict):
101 # Not something we can work with.
102 values[name] = Uninferable
103 continue
105 for dict_key, dict_value in inferred.items:
106 dict_key = safe_infer(dict_key, context=context)
107 if not isinstance(dict_key, nodes.Const):
108 values[name] = Uninferable
109 continue
110 if not isinstance(dict_key.value, str):
111 values[name] = Uninferable
112 continue
113 if dict_key.value in values:
114 # The name is already in the dictionary
115 values[dict_key.value] = Uninferable
116 self.duplicated_keywords.add(dict_key.value)
117 continue
118 values[dict_key.value] = dict_value
119 else:
120 values[name] = value
121 return values
123 def _unpack_args(self, args, context: InferenceContext | None = None):
124 values = []
125 context = context or InferenceContext()
126 context.extra_context = self.argument_context_map
127 for arg in args:
128 if isinstance(arg, nodes.Starred):
129 inferred = safe_infer(arg.value, context=context)
130 if isinstance(inferred, UninferableBase):
131 values.append(Uninferable)
132 continue
133 if not hasattr(inferred, "elts"):
134 values.append(Uninferable)
135 continue
136 values.extend(inferred.elts)
137 else:
138 values.append(arg)
139 return values
141 def infer_argument(
142 self, funcnode: InferenceResult, name: str, context: InferenceContext
143 ): # noqa: C901
144 """Infer a function argument value according to the call context."""
145 if not isinstance(funcnode, (nodes.FunctionDef, nodes.Lambda)):
146 raise InferenceError(
147 f"Can not infer function argument value for non-function node {funcnode!r}.",
148 call_site=self,
149 func=funcnode,
150 arg=name,
151 context=context,
152 )
154 if name in self.duplicated_keywords:
155 raise InferenceError(
156 "The arguments passed to {func!r} have duplicate keywords.",
157 call_site=self,
158 func=funcnode,
159 arg=name,
160 context=context,
161 )
163 # Look into the keywords first, maybe it's already there.
164 try:
165 return self.keyword_arguments[name].infer(context)
166 except KeyError:
167 pass
169 # Too many arguments given and no variable arguments.
170 if len(self.positional_arguments) > len(funcnode.args.args):
171 if not funcnode.args.vararg and not funcnode.args.posonlyargs:
172 raise InferenceError(
173 "Too many positional arguments "
174 "passed to {func!r} that does "
175 "not have *args.",
176 call_site=self,
177 func=funcnode,
178 arg=name,
179 context=context,
180 )
182 positional = self.positional_arguments[: len(funcnode.args.args)]
183 vararg = self.positional_arguments[len(funcnode.args.args) :]
185 # preserving previous behavior, when vararg and kwarg were not included in find_argname results
186 if name in [funcnode.args.vararg, funcnode.args.kwarg]:
187 argindex = None
188 else:
189 argindex = funcnode.args.find_argname(name)[0]
191 kwonlyargs = {arg.name for arg in funcnode.args.kwonlyargs}
192 kwargs = {
193 key: value
194 for key, value in self.keyword_arguments.items()
195 if key not in kwonlyargs
196 }
197 # If there are too few positionals compared to
198 # what the function expects to receive, check to see
199 # if the missing positional arguments were passed
200 # as keyword arguments and if so, place them into the
201 # positional args list.
202 if len(positional) < len(funcnode.args.args):
203 for func_arg in funcnode.args.args:
204 if func_arg.name in kwargs:
205 arg = kwargs.pop(func_arg.name)
206 positional.append(arg)
208 if argindex is not None:
209 boundnode = context.boundnode
210 # 2. first argument of instance/class method
211 if argindex == 0 and funcnode.type in {"method", "classmethod"}:
212 # context.boundnode is None when an instance method is called with
213 # the class, e.g. MyClass.method(obj, ...). In this case, self
214 # is the first argument.
215 if boundnode is None and funcnode.type == "method" and positional:
216 return positional[0].infer(context=context)
217 if boundnode is None:
218 # XXX can do better ?
219 boundnode = funcnode.parent.frame()
221 if isinstance(boundnode, nodes.ClassDef):
222 # Verify that we're accessing a method
223 # of the metaclass through a class, as in
224 # `cls.metaclass_method`. In this case, the
225 # first argument is always the class.
226 method_scope = funcnode.parent.scope()
227 if method_scope is boundnode.metaclass(context=context):
228 return iter((boundnode,))
230 if funcnode.type == "method":
231 if not isinstance(boundnode, Instance):
232 boundnode = boundnode.instantiate_class()
233 return iter((boundnode,))
234 if funcnode.type == "classmethod":
235 return iter((boundnode,))
236 # if we have a method, extract one position
237 # from the index, so we'll take in account
238 # the extra parameter represented by `self` or `cls`
239 if funcnode.type in {"method", "classmethod"} and boundnode:
240 argindex -= 1
241 # 2. search arg index
242 try:
243 return self.positional_arguments[argindex].infer(context)
244 except IndexError:
245 pass
247 if funcnode.args.kwarg == name:
248 # It wants all the keywords that were passed into
249 # the call site.
250 if self.has_invalid_keywords():
251 raise InferenceError(
252 "Inference failed to find values for all keyword arguments "
253 "to {func!r}: {unpacked_kwargs!r} doesn't correspond to "
254 "{keyword_arguments!r}.",
255 keyword_arguments=self.keyword_arguments,
256 unpacked_kwargs=self._unpacked_kwargs,
257 call_site=self,
258 func=funcnode,
259 arg=name,
260 context=context,
261 )
262 kwarg = nodes.Dict(
263 lineno=funcnode.args.lineno,
264 col_offset=funcnode.args.col_offset,
265 parent=funcnode.args,
266 end_lineno=funcnode.args.end_lineno,
267 end_col_offset=funcnode.args.end_col_offset,
268 )
269 kwarg.postinit(
270 [(nodes.const_factory(key), value) for key, value in kwargs.items()]
271 )
272 return iter((kwarg,))
273 if funcnode.args.vararg == name:
274 # It wants all the args that were passed into
275 # the call site.
276 if self.has_invalid_arguments():
277 raise InferenceError(
278 "Inference failed to find values for all positional "
279 "arguments to {func!r}: {unpacked_args!r} doesn't "
280 "correspond to {positional_arguments!r}.",
281 positional_arguments=self.positional_arguments,
282 unpacked_args=self._unpacked_args,
283 call_site=self,
284 func=funcnode,
285 arg=name,
286 context=context,
287 )
288 args = nodes.Tuple(
289 lineno=funcnode.args.lineno,
290 col_offset=funcnode.args.col_offset,
291 parent=funcnode.args,
292 )
293 args.postinit(vararg)
294 return iter((args,))
296 # Check if it's a default parameter.
297 try:
298 return funcnode.args.default_value(name).infer(context)
299 except NoDefault:
300 pass
301 raise InferenceError(
302 "No value found for argument {arg} to {func!r}",
303 call_site=self,
304 func=funcnode,
305 arg=name,
306 context=context,
307 )