summaryrefslogtreecommitdiff
path: root/astroid/arguments.py
diff options
context:
space:
mode:
authorClaudiu Popa <pcmanticore@gmail.com>2015-09-09 00:16:44 +0300
committerClaudiu Popa <pcmanticore@gmail.com>2015-09-09 00:16:44 +0300
commit28ea8b3d8d476e6e74505b6017ad915967619bca (patch)
tree47e78cc0e7ffcbc90744ab5b810865aa8bb34412 /astroid/arguments.py
parent82b6c1508a3228a4b3f00c24ce1a4c9d90f36749 (diff)
downloadastroid-git-28ea8b3d8d476e6e74505b6017ad915967619bca.tar.gz
Improve the understanding of arguments
This changeset introduces a better way to understand arguments passed into call sites. The original logic was moved from astroid.context.CallContext, which become only a container for arguments and keyword arguments, to astroid.arguments.ArgumentInferator, a new class for understanding arguments.
Diffstat (limited to 'astroid/arguments.py')
-rw-r--r--astroid/arguments.py193
1 files changed, 193 insertions, 0 deletions
diff --git a/astroid/arguments.py b/astroid/arguments.py
new file mode 100644
index 00000000..2b5d5013
--- /dev/null
+++ b/astroid/arguments.py
@@ -0,0 +1,193 @@
+# copyright 2003-2015 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
+#
+# This file is part of astroid.
+#
+# astroid is free software: you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by the
+# Free Software Foundation, either version 2.1 of the License, or (at your
+# option) any later version.
+#
+# astroid is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
+# for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License along
+# with astroid. If not, see <http://www.gnu.org/licenses/>.
+
+from astroid import bases
+from astroid import context as contextmod
+from astroid import exceptions
+from astroid import nodes
+from astroid import util
+
+import six
+
+
+class ArgumentInference(object):
+ """Class for understanding arguments passed to functions
+
+ It needs a call context, an object which has the arguments
+ and the keyword arguments that were passed into a given call site.
+ After that, in order to infer what an argument represents, call
+ :meth:`infer_argument` with the corresponding function node
+ and the argument name.
+ """
+
+ def __init__(self, callcontext):
+ self._args = self._unpack_args(callcontext.args)
+ self._keywords = self._unpack_keywords(callcontext.keywords)
+ args = [arg for arg in self._args if arg is not util.YES]
+ keywords = {key: value for key, value in self._keywords.items()
+ if value is not util.YES}
+ self._args_failure = len(args) != len(self._args)
+ self._kwargs_failure = len(keywords) != len(self._keywords)
+ self._args = args
+ self._keywords = keywords
+
+ @staticmethod
+ def _unpack_keywords(keywords):
+ values = {}
+ context = contextmod.InferenceContext()
+ for name, value in keywords:
+ if name is None:
+ # Then it's an unpacking operation (**)
+ try:
+ inferred = next(value.infer(context=context))
+ except exceptions.InferenceError:
+ values[name] = util.YES
+ continue
+
+ if not isinstance(inferred, nodes.Dict):
+ # Not something we can work with.
+ values[name] = util.YES
+ continue
+
+ for dict_key, dict_value in inferred.items:
+ try:
+ dict_key = next(dict_key.infer(context=context))
+ except exceptions.InferenceError:
+ values[name] = util.YES
+ continue
+ if not isinstance(dict_key, nodes.Const):
+ values[name] = util.YES
+ continue
+ if not isinstance(dict_key.value, six.string_types):
+ values[name] = util.YES
+ continue
+ if dict_key.value in values:
+ # The name is already in the dictionary
+ values[name] = util.YES
+ continue
+ values[dict_key.value] = dict_value
+ else:
+ values[name] = value
+ return values
+
+ @staticmethod
+ def _unpack_args(args):
+ values = []
+ context = contextmod.InferenceContext()
+ for arg in args:
+ if isinstance(arg, nodes.Starred):
+ try:
+ inferred = next(arg.value.infer(context=context))
+ except exceptions.InferenceError:
+ values.append(util.YES)
+ continue
+
+ if inferred is util.YES:
+ values.append(util.YES)
+ continue
+ if not hasattr(inferred, 'elts'):
+ values.append(util.YES)
+ 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"""
+ # Look into the keywords first, maybe it's already there.
+ try:
+ return self._keywords[name].infer(context)
+ except KeyError:
+ pass
+
+ # Too many arguments given and no variable arguments.
+ if len(self._args) > len(funcnode.args.args):
+ if not funcnode.args.vararg:
+ raise exceptions.InferenceError(name)
+
+ positional = self._args[:len(funcnode.args.args)]
+ vararg = self._args[len(funcnode.args.args):]
+ argindex = funcnode.args.find_argname(name)[0]
+ kwonlyargs = set(arg.name for arg in funcnode.args.kwonlyargs)
+ kwargs = {key: value for key, value in self._keywords.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:
+ # 2. first argument of instance/class method
+ if argindex == 0 and funcnode.type in ('method', 'classmethod'):
+ if context.boundnode is not None:
+ boundnode = context.boundnode
+ else:
+ # XXX can do better ?
+ boundnode = funcnode.parent.frame()
+ if funcnode.type == 'method':
+ if not isinstance(boundnode, bases.Instance):
+ boundnode = bases.Instance(boundnode)
+ 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'):
+ argindex -= 1
+ # 2. search arg index
+ try:
+ return self._args[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._kwargs_failure:
+ raise exceptions.InferenceError
+ 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, ))
+ elif funcnode.args.vararg == name:
+ # It wants all the args that were passed into
+ # the call site.
+ if self._args_failure:
+ raise exceptions.InferenceError
+ 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 exceptions.NoDefault:
+ pass
+ raise exceptions.InferenceError(name)