| # Licensed under the LGPL: https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html |
| # For details: https://github.com/pylint-dev/astroid/blob/main/LICENSE |
| # Copyright (c) https://github.com/pylint-dev/astroid/blob/main/CONTRIBUTORS.txt |
| |
| from __future__ import annotations |
| |
| from astroid import nodes |
| from astroid.bases import Instance |
| from astroid.context import CallContext, InferenceContext |
| from astroid.exceptions import InferenceError, NoDefault |
| from astroid.typing import InferenceResult |
| from astroid.util import Uninferable, UninferableBase, safe_infer |
| |
| |
| class CallSite: |
| """Class for understanding arguments passed into a call site. |
| |
| It needs a call context, which contains the arguments and the |
| keyword arguments that were passed into a given call site. |
| In order to infer what an argument represents, call :meth:`infer_argument` |
| with the corresponding function node and the argument name. |
| |
| :param callcontext: |
| An instance of :class:`astroid.context.CallContext`, that holds |
| the arguments for the call site. |
| :param argument_context_map: |
| Additional contexts per node, passed in from :attr:`astroid.context.Context.extra_context` |
| :param context: |
| An instance of :class:`astroid.context.Context`. |
| """ |
| |
| def __init__( |
| self, |
| callcontext: CallContext, |
| argument_context_map=None, |
| context: InferenceContext | None = None, |
| ): |
| if argument_context_map is None: |
| argument_context_map = {} |
| self.argument_context_map = argument_context_map |
| args = callcontext.args |
| keywords = callcontext.keywords |
| self.duplicated_keywords: set[str] = set() |
| self._unpacked_args = self._unpack_args(args, context=context) |
| self._unpacked_kwargs = self._unpack_keywords(keywords, context=context) |
| |
| self.positional_arguments = [ |
| arg for arg in self._unpacked_args if not isinstance(arg, UninferableBase) |
| ] |
| self.keyword_arguments = { |
| key: value |
| for key, value in self._unpacked_kwargs.items() |
| if not isinstance(value, UninferableBase) |
| } |
| |
| @classmethod |
| def from_call(cls, call_node, context: InferenceContext | None = None): |
| """Get a CallSite object from the given Call node. |
| |
| context will be used to force a single inference path. |
| """ |
| |
| # Determine the callcontext from the given `context` object if any. |
| context = context or InferenceContext() |
| callcontext = CallContext(call_node.args, call_node.keywords) |
| return cls(callcontext, context=context) |
| |
| def has_invalid_arguments(self): |
| """Check if in the current CallSite were passed *invalid* arguments. |
| |
| This can mean multiple things. For instance, if an unpacking |
| of an invalid object was passed, then this method will return True. |
| Other cases can be when the arguments can't be inferred by astroid, |
| for example, by passing objects which aren't known statically. |
| """ |
| return len(self.positional_arguments) != len(self._unpacked_args) |
| |
| def has_invalid_keywords(self) -> bool: |
| """Check if in the current CallSite were passed *invalid* keyword arguments. |
| |
| For instance, unpacking a dictionary with integer keys is invalid |
| (**{1:2}), because the keys must be strings, which will make this |
| method to return True. Other cases where this might return True if |
| objects which can't be inferred were passed. |
| """ |
| return len(self.keyword_arguments) != len(self._unpacked_kwargs) |
| |
| def _unpack_keywords( |
| self, |
| keywords: list[tuple[str | None, nodes.NodeNG]], |
| context: InferenceContext | None = None, |
| ): |
| values: dict[str | None, InferenceResult] = {} |
| context = context or InferenceContext() |
| context.extra_context = self.argument_context_map |
| for name, value in keywords: |
| if name is None: |
| # Then it's an unpacking operation (**) |
| inferred = safe_infer(value, context=context) |
| if not isinstance(inferred, nodes.Dict): |
| # Not something we can work with. |
| values[name] = Uninferable |
| continue |
| |
| for dict_key, dict_value in inferred.items: |
| dict_key = safe_infer(dict_key, context=context) |
| if not isinstance(dict_key, nodes.Const): |
| values[name] = Uninferable |
| continue |
| if not isinstance(dict_key.value, str): |
| values[name] = Uninferable |
| continue |
| if dict_key.value in values: |
| # The name is already in the dictionary |
| values[dict_key.value] = Uninferable |
| self.duplicated_keywords.add(dict_key.value) |
| continue |
| values[dict_key.value] = dict_value |
| else: |
| values[name] = value |
| return values |
| |
| def _unpack_args(self, args, context: InferenceContext | None = None): |
| values = [] |
| context = context or InferenceContext() |
| context.extra_context = self.argument_context_map |
| for arg in args: |
| if isinstance(arg, nodes.Starred): |
| inferred = safe_infer(arg.value, context=context) |
| if isinstance(inferred, UninferableBase): |
| values.append(Uninferable) |
| continue |
| if not hasattr(inferred, "elts"): |
| values.append(Uninferable) |
| continue |
| values.extend(inferred.elts) |
| else: |
| values.append(arg) |
| return values |
| |
| def infer_argument( |
| self, funcnode: InferenceResult, name: str, context: InferenceContext |
| ): # noqa: C901 |
| """Infer a function argument value according to the call context.""" |
| if not isinstance(funcnode, (nodes.FunctionDef, nodes.Lambda)): |
| raise InferenceError( |
| f"Can not infer function argument value for non-function node {funcnode!r}.", |
| call_site=self, |
| func=funcnode, |
| arg=name, |
| context=context, |
| ) |
| |
| if name in self.duplicated_keywords: |
| raise InferenceError( |
| "The arguments passed to {func!r} have duplicate keywords.", |
| call_site=self, |
| func=funcnode, |
| arg=name, |
| context=context, |
| ) |
| |
| # Look into the keywords first, maybe it's already there. |
| try: |
| return self.keyword_arguments[name].infer(context) |
| except KeyError: |
| pass |
| |
| # Too many arguments given and no variable arguments. |
| if len(self.positional_arguments) > len(funcnode.args.args): |
| if not funcnode.args.vararg and not funcnode.args.posonlyargs: |
| raise InferenceError( |
| "Too many positional arguments " |
| "passed to {func!r} that does " |
| "not have *args.", |
| call_site=self, |
| func=funcnode, |
| arg=name, |
| context=context, |
| ) |
| |
| positional = self.positional_arguments[: len(funcnode.args.args)] |
| vararg = self.positional_arguments[len(funcnode.args.args) :] |
| |
| # preserving previous behavior, when vararg and kwarg were not included in find_argname results |
| if name in [funcnode.args.vararg, funcnode.args.kwarg]: |
| argindex = None |
| else: |
| argindex = funcnode.args.find_argname(name)[0] |
| |
| kwonlyargs = {arg.name for arg in funcnode.args.kwonlyargs} |
| kwargs = { |
| key: value |
| for key, value in self.keyword_arguments.items() |
| if key not in kwonlyargs |
| } |
| # If there are too few positionals compared to |
| # what the function expects to receive, check to see |
| # if the missing positional arguments were passed |
| # as keyword arguments and if so, place them into the |
| # positional args list. |
| if len(positional) < len(funcnode.args.args): |
| for func_arg in funcnode.args.args: |
| if func_arg.name in kwargs: |
| arg = kwargs.pop(func_arg.name) |
| positional.append(arg) |
| |
| if argindex is not None: |
| boundnode = context.boundnode |
| # 2. first argument of instance/class method |
| if argindex == 0 and funcnode.type in {"method", "classmethod"}: |
| # context.boundnode is None when an instance method is called with |
| # the class, e.g. MyClass.method(obj, ...). In this case, self |
| # is the first argument. |
| if boundnode is None and funcnode.type == "method" and positional: |
| return positional[0].infer(context=context) |
| if boundnode is None: |
| # XXX can do better ? |
| boundnode = funcnode.parent.frame() |
| |
| if isinstance(boundnode, nodes.ClassDef): |
| # Verify that we're accessing a method |
| # of the metaclass through a class, as in |
| # `cls.metaclass_method`. In this case, the |
| # first argument is always the class. |
| method_scope = funcnode.parent.scope() |
| if method_scope is boundnode.metaclass(context=context): |
| return iter((boundnode,)) |
| |
| if funcnode.type == "method": |
| if not isinstance(boundnode, Instance): |
| boundnode = boundnode.instantiate_class() |
| return iter((boundnode,)) |
| if funcnode.type == "classmethod": |
| return iter((boundnode,)) |
| # if we have a method, extract one position |
| # from the index, so we'll take in account |
| # the extra parameter represented by `self` or `cls` |
| if funcnode.type in {"method", "classmethod"} and boundnode: |
| argindex -= 1 |
| # 2. search arg index |
| try: |
| return self.positional_arguments[argindex].infer(context) |
| except IndexError: |
| pass |
| |
| if funcnode.args.kwarg == name: |
| # It wants all the keywords that were passed into |
| # the call site. |
| if self.has_invalid_keywords(): |
| raise InferenceError( |
| "Inference failed to find values for all keyword arguments " |
| "to {func!r}: {unpacked_kwargs!r} doesn't correspond to " |
| "{keyword_arguments!r}.", |
| keyword_arguments=self.keyword_arguments, |
| unpacked_kwargs=self._unpacked_kwargs, |
| call_site=self, |
| func=funcnode, |
| arg=name, |
| context=context, |
| ) |
| kwarg = nodes.Dict( |
| lineno=funcnode.args.lineno, |
| col_offset=funcnode.args.col_offset, |
| parent=funcnode.args, |
| end_lineno=funcnode.args.end_lineno, |
| end_col_offset=funcnode.args.end_col_offset, |
| ) |
| kwarg.postinit( |
| [(nodes.const_factory(key), value) for key, value in kwargs.items()] |
| ) |
| return iter((kwarg,)) |
| if funcnode.args.vararg == name: |
| # It wants all the args that were passed into |
| # the call site. |
| if self.has_invalid_arguments(): |
| raise InferenceError( |
| "Inference failed to find values for all positional " |
| "arguments to {func!r}: {unpacked_args!r} doesn't " |
| "correspond to {positional_arguments!r}.", |
| positional_arguments=self.positional_arguments, |
| unpacked_args=self._unpacked_args, |
| call_site=self, |
| func=funcnode, |
| arg=name, |
| context=context, |
| ) |
| args = nodes.Tuple( |
| lineno=funcnode.args.lineno, |
| col_offset=funcnode.args.col_offset, |
| parent=funcnode.args, |
| ) |
| args.postinit(vararg) |
| return iter((args,)) |
| |
| # Check if it's a default parameter. |
| try: |
| return funcnode.args.default_value(name).infer(context) |
| except NoDefault: |
| pass |
| raise InferenceError( |
| "No value found for argument {arg} to {func!r}", |
| call_site=self, |
| func=funcnode, |
| arg=name, |
| context=context, |
| ) |