Tipragot
628be439b8
Cela permet de ne pas avoir de problèmes de compatibilité car python est dans le git.
308 lines
13 KiB
Python
308 lines
13 KiB
Python
# 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,
|
|
)
|