123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316 |
- # Copyright (c) 2015-2016, 2018-2020 Claudiu Popa <pcmanticore@gmail.com>
- # Copyright (c) 2015-2016 Ceridwen <ceridwenv@gmail.com>
- # Copyright (c) 2018 Bryce Guinta <bryce.paul.guinta@gmail.com>
- # Copyright (c) 2018 Nick Drozd <nicholasdrozd@gmail.com>
- # Copyright (c) 2018 Anthony Sottile <asottile@umich.edu>
- # Copyright (c) 2020 hippo91 <guillaume.peillex@gmail.com>
- # Copyright (c) 2021 Pierre Sassoulas <pierre.sassoulas@gmail.com>
- # Copyright (c) 2021 Tushar Sadhwani <86737547+tushar-deepsource@users.noreply.github.com>
- # Copyright (c) 2021 David Liu <david@cs.toronto.edu>
- # Copyright (c) 2021 Marc Mueller <30130371+cdce8p@users.noreply.github.com>
- # 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
- from typing import Optional
- from astroid import nodes
- from astroid.bases import Instance
- from astroid.const import Context
- from astroid.context import CallContext, InferenceContext
- from astroid.exceptions import InferenceError, NoDefault
- from astroid.util import Uninferable
- 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=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()
- 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 arg is not Uninferable
- ]
- self.keyword_arguments = {
- key: value
- for key, value in self._unpacked_kwargs.items()
- if value is not Uninferable
- }
- @classmethod
- def from_call(cls, call_node, context: Optional[Context] = 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):
- """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, context=None):
- values = {}
- 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 (**)
- try:
- inferred = next(value.infer(context=context))
- except InferenceError:
- values[name] = Uninferable
- continue
- except StopIteration:
- continue
- if not isinstance(inferred, nodes.Dict):
- # Not something we can work with.
- values[name] = Uninferable
- continue
- for dict_key, dict_value in inferred.items:
- try:
- dict_key = next(dict_key.infer(context=context))
- except InferenceError:
- values[name] = Uninferable
- continue
- except StopIteration:
- continue
- 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=None):
- values = []
- context = context or InferenceContext()
- context.extra_context = self.argument_context_map
- for arg in args:
- if isinstance(arg, nodes.Starred):
- try:
- inferred = next(arg.value.infer(context=context))
- except InferenceError:
- values.append(Uninferable)
- continue
- except StopIteration:
- continue
- if inferred is Uninferable:
- 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, name, context):
- """infer a function argument value according to the call context
- Arguments:
- funcnode: The function being called.
- name: The name of the argument whose value is being inferred.
- context: Inference context object
- """
- 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) :]
- 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 = getattr(context, "boundnode", None)
- # 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(future=True)
- 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():
- 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,
- )
- 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,
- )
|