summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClaudiu Popa <pcmanticore@gmail.com>2016-02-01 11:53:36 +0000
committerClaudiu Popa <pcmanticore@gmail.com>2016-02-13 14:08:58 +0000
commit6d2529632bae545ff7564501cac14316d5ea9204 (patch)
treecfcb11c01f1ddd4806a24469687a1ebc18bd41f4
parent18fa724c04c2393b134d57d4fe4cebe38472bad8 (diff)
downloadastroid-git-6d2529632bae545ff7564501cac14316d5ea9204.tar.gz
Changed the way how parameters are being built
The old way consisted in having the parameter names, their defaults and their annotations separated in different components of the Arguments node. We introduced a new Param node, which holds the name of a parameter, its default value and its annotation. If any of the last two values are missing, then that slot will be filled with a new node kind, Empty, which is used for specifying the lack of something (None could have been used instead, but that means having non-AST nodes in the Arguments node). We're also having support for positional only arguments, for the moment only in raw_building. Close #215
-rw-r--r--ChangeLog19
-rw-r--r--astroid/inference.py1
-rw-r--r--astroid/interpreter/lookup.py1
-rw-r--r--astroid/interpreter/scope.py21
-rw-r--r--astroid/nodes.py6
-rw-r--r--astroid/protocols.py7
-rw-r--r--astroid/raw_building.py96
-rw-r--r--astroid/test_utils.py13
-rw-r--r--astroid/tests/unittest_brain.py4
-rw-r--r--astroid/tests/unittest_nodes.py3
-rw-r--r--astroid/tests/unittest_python3.py40
-rw-r--r--astroid/tests/unittest_scoped_nodes.py2
-rw-r--r--astroid/tree/base.py2
-rw-r--r--astroid/tree/node_classes.py157
-rw-r--r--astroid/tree/rebuilder.py162
-rw-r--r--astroid/tree/scoped_nodes.py30
-rw-r--r--astroid/tree/treeabc.py13
17 files changed, 349 insertions, 228 deletions
diff --git a/ChangeLog b/ChangeLog
index 251986c1..f405b4e4 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -2,6 +2,25 @@ Change log for the astroid package (used to be astng)
=====================================================
--
+ * Changed the way how parameters are being built
+
+ The old way consisted in having the parameter names, their
+ defaults and their annotations separated in different components
+ of the Arguments node. We introduced a new Param node, which holds
+ the name of a parameter, its default value and its annotation.
+ If any of the last two values are missing, then that slot will be
+ filled with a new node kind, Empty, which is used for specifying the
+ lack of something (None could have been used instead, but that means having
+ non-AST nodes in the Arguments node).
+ We're also having support for positional only arguments, for the moment
+ only in raw_building.
+
+ * We don't support nested arguments in functions in Python 2
+ anymore.
+
+ This was dropped in order to simplify the implementation.
+ When they are encountered, we'll unflatten them into a list
+ of parameters, as if they were not nested from the beginning.
* NodeNG.nearest was removed. It's not an API that we were using
and it was buggy.
diff --git a/astroid/inference.py b/astroid/inference.py
index 1a7ce413..e4074bc1 100644
--- a/astroid/inference.py
+++ b/astroid/inference.py
@@ -698,6 +698,7 @@ def infer_arguments(self, context=None, nodes=None):
@infer.register(treeabc.AssignName)
@infer.register(treeabc.AssignAttr)
+@infer.register(treeabc.Parameter)
@decorators.path_wrapper
def infer_assign(self, context=None):
"""infer a AssignName/AssignAttr: need to inspect the RHS part of the
diff --git a/astroid/interpreter/lookup.py b/astroid/interpreter/lookup.py
index 8a8eb2a0..20fda77a 100644
--- a/astroid/interpreter/lookup.py
+++ b/astroid/interpreter/lookup.py
@@ -238,6 +238,7 @@ def locals_new_scope(node, locals_):
@_get_locals.register(treeabc.DelName)
@_get_locals.register(treeabc.FunctionDef)
@_get_locals.register(treeabc.ClassDef)
+@_get_locals.register(treeabc.Parameter)
def locals_name(node, locals_):
'''These nodes add a name to the local variables. AssignName and
DelName have no children while FunctionDef and ClassDef start a
diff --git a/astroid/interpreter/scope.py b/astroid/interpreter/scope.py
index 0d092ce2..47fb3a0c 100644
--- a/astroid/interpreter/scope.py
+++ b/astroid/interpreter/scope.py
@@ -41,20 +41,23 @@ def _scope_by_parent(parent, node):
# in order to decouple the implementation for the normal cases.
+def _node_arguments(node):
+ for arg in itertools.chain(node.positional_and_keyword, node.keyword_only,
+ (node.vararg, ), (node.kwarg, )):
+ if arg and arg.annotation:
+ yield arg
+
+
@_scope_by_parent.register(treeabc.Arguments)
def _scope_by_argument_parent(parent, node):
args = parent
- if node in itertools.chain(args.defaults, args.kw_defaults):
- return args.parent.parent.scope()
- if six.PY3:
- look_for = itertools.chain(
- (args.kwargannotation, ),
- (args.varargannotation, ),
- args.kwonly_annotations,
- args.annotations)
- if node in look_for:
+ for param in itertools.chain(args.positional_and_keyword, args.keyword_only):
+ if param.default == node:
return args.parent.parent.scope()
+ if six.PY3 and node in _node_arguments(args):
+ return args.parent.parent.scope()
+
@_scope_by_parent.register(treeabc.FunctionDef)
def _scope_by_function_parent(parent, node):
diff --git a/astroid/nodes.py b/astroid/nodes.py
index 1aea53f2..6fb37652 100644
--- a/astroid/nodes.py
+++ b/astroid/nodes.py
@@ -40,9 +40,9 @@ from astroid.tree.node_classes import (
Arguments, AssignAttr, Assert, Assign,
AssignName, AugAssign, Repr, BinOp, BoolOp, Break, Call, Compare,
Comprehension, Const, Continue, Decorators, DelAttr, DelName, Delete,
- Dict, Expr, Ellipsis, ExceptHandler, Exec, ExtSlice, For,
+ Dict, Empty, Expr, Ellipsis, ExceptHandler, Exec, ExtSlice, For,
ImportFrom, Attribute, Global, If, IfExp, Import, Index, Keyword,
- List, Name, NameConstant, Nonlocal, Pass, Print, Raise, Return, Set, Slice,
+ List, Name, NameConstant, Nonlocal, Pass, Parameter, Print, Raise, Return, Set, Slice,
Starred, Subscript, TryExcept, TryFinally, Tuple, UnaryOp, While, With,
WithItem, Yield, YieldFrom, AsyncFor, Await, AsyncWith,
# Node not present in the builtin ast module.
@@ -74,7 +74,7 @@ ALL_NODE_CLASSES = (
Lambda, List, ListComp,
Name, NameConstant, Nonlocal,
Module,
- Pass, Print,
+ Parameter, Pass, Print,
Raise, ReservedName, Return,
Set, SetComp, Slice, Starred, Subscript,
TryExcept, TryFinally, Tuple,
diff --git a/astroid/protocols.py b/astroid/protocols.py
index eada9764..7fdc6e29 100644
--- a/astroid/protocols.py
+++ b/astroid/protocols.py
@@ -304,6 +304,7 @@ def mulass_assigned_stmts(self, nodes, node=None, context=None, assign_path=None
@assigned_stmts.register(treeabc.AssignName)
@assigned_stmts.register(treeabc.AssignAttr)
+@assigned_stmts.register(treeabc.Parameter)
def assend_assigned_stmts(self, nodes, node=None, context=None, assign_path=None):
return self.parent.assigned_stmts(self, context=context)
@@ -311,7 +312,7 @@ def assend_assigned_stmts(self, nodes, node=None, context=None, assign_path=None
def _arguments_infer_argname(self, name, context, nodes):
# arguments information may be missing, in which case we can't do anything
# more
- if not (self.args or self.vararg or self.kwarg):
+ if not self.args and (not self.vararg and not self.kwarg):
yield util.Uninferable
return
# first argument of instance/class method
@@ -336,10 +337,10 @@ def _arguments_infer_argname(self, name, context, nodes):
return
# TODO: just provide the type here, no need to have an empty Dict.
- if name == self.vararg:
+ if self.vararg and name == self.vararg.name:
yield nodes.Tuple(parent=self)
return
- if name == self.kwarg:
+ if self.kwarg and name == self.kwarg.name:
yield nodes.Dict(parent=self)
return
# if there is a default value, yield it. And then yield Uninferable to reflect
diff --git a/astroid/raw_building.py b/astroid/raw_building.py
index f3871143..bccd83da 100644
--- a/astroid/raw_building.py
+++ b/astroid/raw_building.py
@@ -319,65 +319,71 @@ def ast_from_function(func, built_objects, module, name=None, parent=None):
itertools.groupby(signature.parameters.values(),
operator.attrgetter('kind'))}
- def extract_args(parameters, parent):
- '''Takes an iterator over Parameter objects and returns three
- sequences, arg names, default values, and annotations.
-
- '''
- names = []
- defaults = []
- annotations = []
+ def _extract_args(parameters, parent):
+ """Generate an iterator of Parameter nodes from a list of inspect.Parameter objects."""
for parameter in parameters:
- names.append(parameter.name)
+ param = node_classes.Parameter(name=parameter.name,
+ col_offset=parent.col_offset,
+ lineno=parent.lineno,
+ parent=parent)
+ default = node_classes.Empty
+ annotation = node_classes.Empty
if parameter.default is not _Parameter.empty:
- defaults.extend(_ast_from_object(parameter.default, built_objects, module, parent=parent))
+ default = _ast_from_object(parameter.default, built_objects,
+ module, parent=parent)
+
if parameter.annotation is not _Parameter.empty:
- annotations.extend(_ast_from_object(parameter.annotation, built_objects, module, parent=parent))
- else:
- annotations.append(None)
- return names, defaults, annotations
+ annotation = _ast_from_object(parameter.annotation, built_objects,
+ module, parent=parent)
- def extract_vararg(parameter):
- '''Takes a single-element iterator possibly containing a Parameter and
- returns a name and an annotation.
+ param.postinit(default=default, annotation=annotation)
+ yield param
- '''
+ def _extract_vararg(parameter, parent):
+ """Build a variadic Parameter node from an inspect.Parameter object."""
try:
- return parameter[0].name
+ parameter = parameter[0]
except IndexError:
- return None
-
- vararg = parameters.get(_Parameter.VAR_POSITIONAL, ())
- kwarg = parameters.get(_Parameter.VAR_KEYWORD, ())
- vararg_name = extract_vararg(vararg)
- kwarg_name = extract_vararg(kwarg)
- args_node = node_classes.Arguments(vararg=vararg_name, kwarg=kwarg_name, parent=func_node)
-
- # This ignores POSITIONAL_ONLY args, because they only appear in
- # functions implemented in C and can't be mimicked by any Python
- # function.
- names, defaults, annotations = extract_args(parameters.get(_Parameter.POSITIONAL_OR_KEYWORD, ()), args_node)
- kwonlynames, kw_defaults, kwonly_annotations = extract_args(parameters.get(_Parameter.KEYWORD_ONLY, ()), args_node)
- args = [node_classes.AssignName(name=n, parent=args_node) for n in names]
- kwonlyargs = [node_classes.AssignName(name=n, parent=args_node) for n in kwonlynames]
- if vararg_name and vararg[0].annotation is not _Parameter.empty:
- varargannotation = vararg.annotation
- else:
- varargannotation = None
- if kwarg_name and kwarg[0].annotation is not _Parameter.empty:
- kwargannotation = kwarg.annotation
- else:
- kwargannotation = None
+ return node_classes.Empty
+
+ if parameter.annotation is not _Parameter.empty:
+ annotation = _ast_from_object(parameter.annotation,
+ built_objects, module, parent=parent)[0]
+ else:
+ annotation = node_classes.Empty
+
+ param = node_classes.Parameter(name=parameter.name,
+ lineno=parent.lineno,
+ col_offset=parent.col_offset,
+ parent=parent)
+ param.postinit(annotation=annotation, default=node_classes.Empty)
+ return param
+
+ args_node = node_classes.Arguments(parent=func_node)
+ args = _extract_args(parameters.get(_Parameter.POSITIONAL_OR_KEYWORD, ()),
+ args_node)
+ keyword_only = _extract_args(parameters.get(_Parameter.KEYWORD_ONLY, ()),
+ args_node)
+ positional_only = _extract_args(parameters.get(_Parameter.POSITIONAL_ONLY, ()),
+ args_node)
+ python_vararg = parameters.get(_Parameter.VAR_POSITIONAL, ())
+ python_kwarg = parameters.get(_Parameter.VAR_KEYWORD, ())
+ vararg = _extract_vararg(python_vararg, args_node)
+ kwarg = _extract_vararg(python_kwarg, args_node)
+
returns = None
if signature.return_annotation is not _Parameter.empty:
returns = _ast_from_object(signature.return_annotation,
built_objects,
module,
parent=func_node)[0]
- args_node.postinit(args, defaults, kwonlyargs, kw_defaults,
- annotations, kwonly_annotations,
- varargannotation, kwargannotation)
+ args_node.postinit(args=list(args),
+ vararg=vararg,
+ kwarg=kwarg,
+ keyword_only=list(keyword_only),
+ positional_only=list(positional_only))
func_node.postinit(args=args_node, body=[], returns=returns)
+
for name in set(dir(func)) - set(dir(type(func))):
# This checks against method special attributes because
# methods are also dispatched through this function.
diff --git a/astroid/test_utils.py b/astroid/test_utils.py
index b15ab766..ff81cd59 100644
--- a/astroid/test_utils.py
+++ b/astroid/test_utils.py
@@ -19,6 +19,7 @@ _TRANSIENT_FUNCTION = '__'
# when calling extract_node.
_STATEMENT_SELECTOR = '#@'
+
def _extract_expressions(node):
"""Find expressions in a call to _TRANSIENT_FUNCTION and extract them.
@@ -46,8 +47,18 @@ def _extract_expressions(node):
child = getattr(node.parent, name)
if isinstance(child, (list, tuple)):
for idx, compound_child in enumerate(child):
- if compound_child is node:
+
+ # Can't find a cleaner way to do this.
+ if isinstance(compound_child, nodes.Parameter):
+ if compound_child.default is node:
+ child[idx].default = real_expr
+ elif compound_child.annotation is node:
+ child[idx].annotation = real_expr
+ else:
+ child[idx] = real_expr
+ elif compound_child is node:
child[idx] = real_expr
+
elif child is node:
setattr(node.parent, name, real_expr)
yield real_expr
diff --git a/astroid/tests/unittest_brain.py b/astroid/tests/unittest_brain.py
index 85f6f04c..a12b242e 100644
--- a/astroid/tests/unittest_brain.py
+++ b/astroid/tests/unittest_brain.py
@@ -84,7 +84,9 @@ class HashlibTest(unittest.TestCase):
self.assertIn('block_size', class_obj)
self.assertIn('digest_size', class_obj)
self.assertEqual(len(class_obj['__init__'].args.args), 2)
- self.assertEqual(len(class_obj['__init__'].args.defaults), 1)
+ default = class_obj['__init__'].args.args[1].default
+ self.assertIsInstance(default, nodes.Const)
+ self.assertEqual(default.value, '')
self.assertEqual(len(class_obj['update'].args.args), 2)
self.assertEqual(len(class_obj['digest'].args.args), 1)
self.assertEqual(len(class_obj['hexdigest'].args.args), 1)
diff --git a/astroid/tests/unittest_nodes.py b/astroid/tests/unittest_nodes.py
index fab82eef..5b89043a 100644
--- a/astroid/tests/unittest_nodes.py
+++ b/astroid/tests/unittest_nodes.py
@@ -491,6 +491,9 @@ class NameNodeTest(unittest.TestCase):
class ArgumentsNodeTC(unittest.TestCase):
+
+ @unittest.skipIf(sys.version_info[:2] == (3, 3),
+ "Line numbering is broken on Python 3.3.")
def test_linenumbering(self):
ast = builder.parse('''
def func(a,
diff --git a/astroid/tests/unittest_python3.py b/astroid/tests/unittest_python3.py
index 1dd07c61..3cd7b1f5 100644
--- a/astroid/tests/unittest_python3.py
+++ b/astroid/tests/unittest_python3.py
@@ -20,6 +20,7 @@ import unittest
from astroid import nodes
from astroid.tree.node_classes import Assign, Expr, YieldFrom, Name, Const
+from astroid import raw_building
from astroid.builder import AstroidBuilder
from astroid.tree.scoped_nodes import ClassDef, FunctionDef
from astroid.test_utils import require_version, extract_node
@@ -187,31 +188,31 @@ class Python3TC(unittest.TestCase):
pass
"""))
func = astroid['test']
- self.assertIsInstance(func.args.varargannotation, Name)
- self.assertEqual(func.args.varargannotation.name, 'float')
- self.assertIsInstance(func.args.kwargannotation, Name)
- self.assertEqual(func.args.kwargannotation.name, 'int')
+ self.assertIsInstance(func.args.vararg.annotation, Name)
+ self.assertEqual(func.args.vararg.annotation.name, 'float')
+ self.assertIsInstance(func.args.kwarg.annotation, Name)
+ self.assertEqual(func.args.kwarg.annotation.name, 'int')
self.assertIsInstance(func.returns, Name)
self.assertEqual(func.returns.name, 'int')
arguments = func.args
- self.assertIsInstance(arguments.annotations[0], Name)
- self.assertEqual(arguments.annotations[0].name, 'int')
- self.assertIsInstance(arguments.annotations[1], Name)
- self.assertEqual(arguments.annotations[1].name, 'str')
- self.assertIsInstance(arguments.annotations[2], Const)
- self.assertIsNone(arguments.annotations[2].value)
- self.assertIsNone(arguments.annotations[3])
- self.assertIsNone(arguments.annotations[4])
+ self.assertIsInstance(arguments.args[0].annotation, Name)
+ self.assertEqual(arguments.args[0].annotation.name, 'int')
+ self.assertIsInstance(arguments.args[1].annotation, Name)
+ self.assertEqual(arguments.args[1].annotation.name, 'str')
+ self.assertIsInstance(arguments.args[2].annotation, Const)
+ self.assertIsNone(arguments.args[2].annotation.value)
+ self.assertIs(arguments.args[3].annotation, nodes.Empty)
+ self.assertIs(arguments.args[4].annotation, nodes.Empty)
astroid = self.builder.string_build(dedent("""
def test(a: int=1, b: str=2):
pass
"""))
func = astroid['test']
- self.assertIsInstance(func.args.annotations[0], Name)
- self.assertEqual(func.args.annotations[0].name, 'int')
- self.assertIsInstance(func.args.annotations[1], Name)
- self.assertEqual(func.args.annotations[1].name, 'str')
+ self.assertIsInstance(func.args.args[0].annotation, Name)
+ self.assertEqual(func.args.args[0].annotation.name, 'int')
+ self.assertIsInstance(func.args.args[1].annotation, Name)
+ self.assertEqual(func.args.args[1].annotation.name, 'str')
self.assertIsNone(func.returns)
@require_version('3.0')
@@ -249,6 +250,13 @@ class Python3TC(unittest.TestCase):
self.assertIsInstance(value, nodes.Const)
self.assertEqual(value.value, expected)
+ @require_version('3.4')
+ def test_positional_only_parameters(self):
+ ast = raw_building.ast_from_object(issubclass)
+ self.assertEqual(len(ast.args.positional_only), 2)
+ for name, arg in zip(('cls', 'class_or_tuple'), ast.args.positional_only):
+ self.assertEqual(arg.name, name)
+
if __name__ == '__main__':
unittest.main()
diff --git a/astroid/tests/unittest_scoped_nodes.py b/astroid/tests/unittest_scoped_nodes.py
index d9600365..462a1f20 100644
--- a/astroid/tests/unittest_scoped_nodes.py
+++ b/astroid/tests/unittest_scoped_nodes.py
@@ -284,7 +284,7 @@ class FunctionNodeTest(ModuleLoader, unittest.TestCase):
tree = builder.parse(code)
func = tree['nested_args']
self.assertEqual(sorted(func.locals), ['a', 'b', 'c', 'd'])
- self.assertEqual(func.args.format_args(), 'a, (b, c, d)')
+ self.assertEqual(func.args.format_args(), 'a, b, c, d')
def test_four_args(self):
func = self.module['four_args']
diff --git a/astroid/tree/base.py b/astroid/tree/base.py
index de137560..58298b8d 100644
--- a/astroid/tree/base.py
+++ b/astroid/tree/base.py
@@ -633,7 +633,7 @@ class LookupMixIn(object):
if not (optional_assign or interpreterutil.are_exclusive(_stmts[pindex], node)):
del _stmt_parents[pindex]
del _stmts[pindex]
- if isinstance(node, treeabc.AssignName):
+ if isinstance(node, (treeabc.Parameter, treeabc.AssignName)):
if not optional_assign and stmt.parent is mystmt.parent:
_stmts = []
_stmt_parents = []
diff --git a/astroid/tree/node_classes.py b/astroid/tree/node_classes.py
index 722e2b75..7189b068 100644
--- a/astroid/tree/node_classes.py
+++ b/astroid/tree/node_classes.py
@@ -98,18 +98,36 @@ class AssignedStmtsMixin(object):
# Name classes
-@util.register_implementation(treeabc.AssignName)
-class AssignName(base.LookupMixIn, base.ParentAssignTypeMixin,
- AssignedStmtsMixin, base.NodeNG):
- """class representing an AssignName node"""
+
+class BaseAssignName(base.LookupMixIn, base.ParentAssignTypeMixin,
+ AssignedStmtsMixin, base.NodeNG):
_other_fields = ('name',)
def __init__(self, name=None, lineno=None, col_offset=None, parent=None):
self.name = name
- super(AssignName, self).__init__(lineno, col_offset, parent)
+ super(BaseAssignName, self).__init__(lineno, col_offset, parent)
infer_lhs = inference.infer_name
+@util.register_implementation(treeabc.AssignName)
+class AssignName(BaseAssignName):
+ """class representing an AssignName node"""
+
+
+@util.register_implementation(treeabc.Parameter)
+class Parameter(BaseAssignName):
+
+ _astroid_fields = ('default', 'annotation')
+ _other_fields = ('name', )
+
+ def __init__(self, name=None, lineno=None, col_offset=None, parent=None):
+ super(Parameter, self).__init__(name=name, lineno=lineno,
+ col_offset=col_offset, parent=parent)
+
+ def postinit(self, default, annotation):
+ self.default = default
+ self.annotation = annotation
+
@util.register_implementation(treeabc.DelName)
class DelName(base.LookupMixIn, base.ParentAssignTypeMixin, base.NodeNG):
@@ -129,54 +147,25 @@ class Name(base.LookupMixIn, base.NodeNG):
def __init__(self, name=None, lineno=None, col_offset=None, parent=None):
self.name = name
super(Name, self).__init__(lineno, col_offset, parent)
-
+
@util.register_implementation(treeabc.Arguments)
class Arguments(base.AssignTypeMixin, AssignedStmtsMixin, base.NodeNG):
"""class representing an Arguments node"""
- if six.PY3:
- # Python 3.4+ uses a different approach regarding annotations,
- # each argument is a new class, _ast.arg, which exposes an
- # 'annotation' attribute. In astroid though, arguments are exposed
- # as is in the Arguments node and the only way to expose annotations
- # is by using something similar with Python 3.3:
- # - we expose 'varargannotation' and 'kwargannotation' of annotations
- # of varargs and kwargs.
- # - we expose 'annotation', a list with annotations for
- # for each normal argument. If an argument doesn't have an
- # annotation, its value will be None.
-
- _astroid_fields = ('args', 'defaults', 'kwonlyargs',
- 'kw_defaults', 'annotations', 'kwonly_annotations',
- 'varargannotation', 'kwargannotation')
- varargannotation = None
- kwargannotation = None
- else:
- _astroid_fields = ('args', 'defaults', 'kwonlyargs', 'kw_defaults')
- _other_fields = ('vararg', 'kwarg')
- def __init__(self, vararg=None, kwarg=None, parent=None):
+ _astroid_fields = ('args', 'vararg', 'kwarg', 'keyword_only', 'positional_only')
+
+ def __init__(self, parent=None):
+ # We don't want lineno and col_offset from the parent's __init__.
+ super(Arguments, self).__init__(parent=parent)
+
+ def postinit(self, args, vararg, kwarg, keyword_only, positional_only):
+ self.args = args
self.vararg = vararg
self.kwarg = kwarg
- self.parent = parent
- self.args = []
- self.defaults = []
- self.kwonlyargs = []
- self.kw_defaults = []
- self.annotations = []
- self.kwonly_annotations = []
-
- def postinit(self, args, defaults, kwonlyargs, kw_defaults,
- annotations, kwonly_annotations, varargannotation=None,
- kwargannotation=None):
- self.args = args
- self.defaults = defaults
- self.kwonlyargs = kwonlyargs
- self.kw_defaults = kw_defaults
- self.annotations = annotations
- self.varargannotation = varargannotation
- self.kwargannotation = kwargannotation
- self.kwonly_annotations = kwonly_annotations
+ self.keyword_only = keyword_only
+ self.positional_only = positional_only
+ self.positional_and_keyword = self.args + self.positional_only
def _infer_name(self, frame, name):
if self.parent is frame:
@@ -194,19 +183,16 @@ class Arguments(base.AssignTypeMixin, AssignedStmtsMixin, base.NodeNG):
def format_args(self):
"""return arguments formatted as string"""
result = []
- if self.args:
- result.append(
- _format_args(self.args, self.defaults,
- getattr(self, 'annotations', None))
- )
+ if self.positional_and_keyword:
+ result.append(_format_args(self.positional_and_keyword))
if self.vararg:
- result.append('*%s' % self.vararg)
- if self.kwonlyargs:
+ result.append('*%s' % _format_args((self.vararg, )))
+ if self.keyword_only:
if not self.vararg:
result.append('*')
- result.append(_format_args(self.kwonlyargs, self.kw_defaults))
+ result.append(_format_args(self.keyword_only))
if self.kwarg:
- result.append('**%s' % self.kwarg)
+ result.append('**%s' % _format_args((self.kwarg, )))
return ', '.join(result)
def default_value(self, argname):
@@ -214,28 +200,28 @@ class Arguments(base.AssignTypeMixin, AssignedStmtsMixin, base.NodeNG):
:raise `NoDefault`: if there is no default value defined
"""
- i = _find_arg(argname, self.args)[0]
- if i is not None:
- idx = i - (len(self.args) - len(self.defaults))
- if idx >= 0:
- return self.defaults[idx]
- i = _find_arg(argname, self.kwonlyargs)[0]
- if i is not None and self.kw_defaults[i] is not None:
- return self.kw_defaults[i]
+ for place in (self.positional_and_keyword, self.keyword_only):
+ i = _find_arg(argname, place)[0]
+ if i is not None:
+ value = place[i]
+ if not value.default:
+ continue
+ return value.default
+
raise exceptions.NoDefault(func=self.parent, name=argname)
def is_argument(self, name):
"""return True if the name is defined in arguments"""
- if name == self.vararg:
+ if self.vararg and name == self.vararg.name:
return True
- if name == self.kwarg:
+ if self.kwarg and name == self.kwarg.name:
return True
return self.find_argname(name, True)[1] is not None
def find_argname(self, argname, rec=False):
"""return index and Name node with given name"""
- if self.args: # self.args may be None in some cases (builtin function)
- return _find_arg(argname, self.args, rec)
+ if self.positional_and_keyword: # self.args may be None in some cases (builtin function)
+ return _find_arg(argname, self.positional_and_keyword, rec)
return None, None
def get_children(self):
@@ -257,27 +243,24 @@ def _find_arg(argname, args, rec=False):
return None, None
-def _format_args(args, defaults=None, annotations=None):
+def _format_args(args):
values = []
- if args is None:
+ if not args:
return ''
- if annotations is None:
- annotations = []
- if defaults is not None:
- default_offset = len(args) - len(defaults)
- packed = six.moves.zip_longest(args, annotations)
- for i, (arg, annotation) in enumerate(packed):
+ for i, arg in enumerate(args):
if isinstance(arg, Tuple):
values.append('(%s)' % _format_args(arg.elts))
else:
argname = arg.name
- if annotation is not None:
+ annotation = arg.annotation
+ if annotation:
argname += ':' + annotation.as_string()
values.append(argname)
+
+ default = arg.default
+ if default:
+ values[-1] += '=' + default.as_string()
- if defaults is not None and i >= default_offset:
- if defaults[i-default_offset] is not None:
- values[-1] += '=' + defaults[i-default_offset].as_string()
return ', '.join(values)
@@ -1322,6 +1305,22 @@ class DictUnpack(base.NodeNG):
"""Represents the unpacking of dicts into dicts using PEP 448."""
+@object.__new__
+@util.register_implementation(treeabc.Empty)
+class Empty(base.NodeNG):
+ """Empty nodes represents the lack of something
+
+ For instance, they can be used to represent missing annotations
+ or defaults for arguments or anything where None is a valid
+ value.
+ """
+
+ def __bool__(self):
+ return False
+
+ __nonzero__ = __bool__
+
+
# Register additional inference dispatched functions. We do
# this here, since we need to pass this module as an argument
# to these functions, in order to avoid circular dependencies
diff --git a/astroid/tree/rebuilder.py b/astroid/tree/rebuilder.py
index 90a02f73..074aea1e 100644
--- a/astroid/tree/rebuilder.py
+++ b/astroid/tree/rebuilder.py
@@ -20,6 +20,7 @@ order to get a single Astroid representation
"""
import ast
+import collections
import sys
import astroid
@@ -113,6 +114,53 @@ def _get_context(node):
return CONTEXTS.get(type(node.ctx), astroid.Load)
+class ParameterVisitor(object):
+ """A visitor which is used for building the components of Arguments node."""
+
+ def __init__(self, visitor):
+ self._visitor = visitor
+
+ def visit(self, param_node, *args):
+ cls_name = param_node.__class__.__name__
+ visit_name = 'visit_' + REDIRECT.get(cls_name, cls_name).lower()
+ visit_method = getattr(self, visit_name)
+ return visit_method(param_node, *args)
+
+ def visit_arg(self, param_node, *args):
+ name = param_node.arg
+ return self._build_parameter(param_node, name, *args)
+
+ def visit_name(self, param_node, *args):
+ name = param_node.id
+ return self._build_parameter(param_node, name, *args)
+
+ def visit_tuple(self, param_node, parent, default):
+ # We're not supporting nested arguments anymore, but in order to
+ # simply not crash when running on Python 2, we're unpacking the elements
+ # before hand. We simply don't want to support this feature anymore,
+ # so it's possible to be broken.
+ converted_node = self._visitor.visit(param_node, parent)
+ for element in converted_node.elts:
+ param = nodes.Parameter(name=element.name, lineno=param_node.lineno,
+ col_offset=param_node.col_offset,
+ parent=parent)
+ param.postinit(default=default, annotation=nodes.Empty)
+ yield param
+
+ def _build_parameter(self, param_node, name, parent, default):
+ param = nodes.Parameter(name=name, lineno=getattr(param_node, 'lineno', None),
+ col_offset=getattr(param_node, 'col_offset', None),
+ parent=parent)
+ annotation = nodes.Empty
+ param_annotation = getattr(param_node, 'annotation', None)
+ if param_annotation:
+ annotation = self._visitor.visit(param_annotation, param)
+
+ param.postinit(default=default, annotation=annotation)
+ yield param
+
+
+
class TreeRebuilder(object):
"""Rebuilds the ast tree to become an Astroid tree"""
@@ -141,57 +189,64 @@ class TreeRebuilder(object):
def visit_arguments(self, node, parent):
"""visit a Arguments node by returning a fresh instance of it"""
- vararg, kwarg = node.vararg, node.kwarg
- if PY34:
- newnode = nodes.Arguments(vararg.arg if vararg else None,
- kwarg.arg if kwarg else None,
- parent)
- else:
- newnode = nodes.Arguments(vararg, kwarg, parent)
- args = [self.visit(child, newnode) for child in node.args]
- defaults = [self.visit(child, newnode)
- for child in node.defaults]
- varargannotation = None
- kwargannotation = None
- # change added in 82732 (7c5c678e4164), vararg and kwarg
- # are instances of `ast.arg`, not strings
- if vararg:
- if PY34:
- if node.vararg.annotation:
- varargannotation = self.visit(node.vararg.annotation,
- newnode)
- vararg = vararg.arg
- elif PY3 and node.varargannotation:
- varargannotation = self.visit(node.varargannotation,
- newnode)
- if kwarg:
- if PY34:
- if node.kwarg.annotation:
- kwargannotation = self.visit(node.kwarg.annotation,
- newnode)
- kwarg = kwarg.arg
- elif PY3:
- if node.kwargannotation:
- kwargannotation = self.visit(node.kwargannotation,
- newnode)
- if PY3:
- kwonlyargs = [self.visit(child, newnode) for child
- in node.kwonlyargs]
- kw_defaults = [self.visit(child, newnode) if child else
- None for child in node.kw_defaults]
- annotations = [self.visit(arg.annotation, newnode) if
- arg.annotation else None for arg in node.args]
- kwonly_annotations = [self.visit(arg.annotation, newnode)
- if arg.annotation else None
- for arg in node.kwonlyargs]
- else:
- kwonlyargs = []
- kw_defaults = []
- annotations = []
- kwonly_annotations = []
- newnode.postinit(args, defaults, kwonlyargs, kw_defaults,
- annotations, kwonly_annotations,
- varargannotation, kwargannotation)
+ def _build_variadic(field_name):
+ param = nodes.Empty
+ variadic = getattr(node, field_name)
+
+ if variadic:
+ # Various places to get the name from.
+ try:
+ param_name = variadic.id
+ except AttributeError:
+ try:
+ param_name = variadic.arg
+ except AttributeError:
+ param_name = variadic
+
+ param = nodes.Parameter(name=param_name,
+ lineno=newnode.lineno,
+ col_offset=newnode.col_offset,
+ parent=newnode)
+ # Get the annotation of the variadic node.
+ annotation = nodes.Empty
+ default = nodes.Empty
+ variadic_annotation = getattr(variadic, 'annotation', None)
+ if variadic_annotation is None:
+ # Support for Python 3.3.
+ variadic_annotation = getattr(node, field_name + 'annotation', None)
+ if variadic_annotation:
+ annotation = self.visit(variadic_annotation, param)
+
+ param.postinit(default=default, annotation=annotation)
+ return param
+
+ def _build_args(params, defaults):
+ # Pad the list of defaults so that each arguments gets a default.
+ defaults = collections.deque(defaults)
+ while len(defaults) != len(params):
+ defaults.appendleft(nodes.Empty)
+
+ param_visitor = ParameterVisitor(self)
+ for parameter in params:
+ default = defaults.popleft()
+ if default:
+ default = self.visit(default, newnode)
+
+ for param in param_visitor.visit(parameter, newnode, default):
+ yield param
+
+ newnode = nodes.Arguments(parent=parent)
+ # Build the arguments list.
+ positional_args = list(_build_args(node.args, node.defaults))
+ kwonlyargs = list(_build_args(getattr(node, 'kwonlyargs', ()),
+ getattr(node, 'kw_defaults', ())))
+ # Build vararg and kwarg.
+ vararg = _build_variadic('vararg')
+ kwarg = _build_variadic('kwarg')
+ # Prepare the arguments new node.
+ newnode.postinit(args=positional_args, vararg=vararg, kwarg=kwarg,
+ keyword_only=kwonlyargs,
+ positional_only=[])
return newnode
def visit_assert(self, node, parent):
@@ -729,11 +784,6 @@ class TreeRebuilder(object):
class TreeRebuilder3(TreeRebuilder):
"""extend and overwrite TreeRebuilder for python3k"""
- def visit_arg(self, node, parent):
- """visit a arg node by returning a fresh AssName instance"""
- # TODO(cpopa): introduce an Arg node instead of using AssignName.
- return self.visit_assignname(node, parent, node.arg)
-
def visit_nameconstant(self, node, parent):
# in Python 3.4 we have NameConstant for True / False / None
return nodes.NameConstant(node.value, getattr(node, 'lineno', None),
diff --git a/astroid/tree/scoped_nodes.py b/astroid/tree/scoped_nodes.py
index d04b549b..6744ec82 100644
--- a/astroid/tree/scoped_nodes.py
+++ b/astroid/tree/scoped_nodes.py
@@ -686,7 +686,7 @@ class CallSite(object):
pass
# Too many arguments given and no variable arguments.
- if len(self.positional_arguments) > len(self._funcnode.args.args):
+ if len(self.positional_arguments) > len(self._funcnode.args.positional_and_keyword):
if not self._funcnode.args.vararg:
raise exceptions.InferenceError('Too many positional arguments '
'passed to {func!r} that does '
@@ -694,10 +694,10 @@ class CallSite(object):
call_site=self, func=self._funcnode,
arg=name, context=context)
- positional = self.positional_arguments[:len(self._funcnode.args.args)]
- vararg = self.positional_arguments[len(self._funcnode.args.args):]
+ positional = self.positional_arguments[:len(self._funcnode.args.positional_and_keyword)]
+ vararg = self.positional_arguments[len(self._funcnode.args.positional_and_keyword):]
argindex = self._funcnode.args.find_argname(name)[0]
- kwonlyargs = set(arg.name for arg in self._funcnode.args.kwonlyargs)
+ kwonlyargs = set(arg.name for arg in self._funcnode.args.keyword_only)
kwargs = {
key: value for key, value in self.keyword_arguments.items()
if key not in kwonlyargs
@@ -707,8 +707,8 @@ class CallSite(object):
# if the missing positional arguments were passed
# as keyword arguments and if so, place them into the
# positional args list.
- if len(positional) < len(self._funcnode.args.args):
- for func_arg in self._funcnode.args.args:
+ if len(positional) < len(self._funcnode.args.positional_and_keyword):
+ for func_arg in self._funcnode.args.positional_and_keyword:
if func_arg.name in kwargs:
arg = kwargs.pop(func_arg.name)
positional.append(arg)
@@ -748,7 +748,7 @@ class CallSite(object):
except IndexError:
pass
- if self._funcnode.args.kwarg == name:
+ if self._funcnode.args.kwarg and self._funcnode.args.kwarg.name == name:
# It wants all the keywords that were passed into
# the call site.
if self.has_invalid_keywords():
@@ -769,7 +769,7 @@ class CallSite(object):
kwarg.postinit(keys, values)
return iter((kwarg, ))
- elif self._funcnode.args.vararg == name:
+ if self._funcnode.args.vararg and self._funcnode.args.vararg.name == name:
# It wants all the args that were passed into
# the call site.
if self.has_invalid_arguments():
@@ -813,7 +813,9 @@ class LambdaFunctionMixin(QualifiedNameMixin, base.FilterStmtsMixin):
return CallSite(self, args, keywords)
def scope_lookup(self, node, name, offset=0):
- if node in self.args.defaults or node in self.args.kw_defaults:
+
+ if node in itertools.chain((self.args.positional_and_keyword,
+ self.args.keyword_only)):
frame = self.parent.frame()
# line offset to avoid that def func(f=func) resolve the default
# value to the defined function
@@ -825,14 +827,16 @@ class LambdaFunctionMixin(QualifiedNameMixin, base.FilterStmtsMixin):
def argnames(self):
"""return a list of argument names"""
- if self.args.args: # maybe None with builtin functions
- names = _rec_get_names(self.args.args)
+ if self.args.positional_and_keyword: # maybe None with builtin functions
+ names = _rec_get_names(self.args.positional_and_keyword)
else:
names = []
if self.args.vararg:
- names.append(self.args.vararg)
+ names.append(self.args.vararg.name)
if self.args.kwarg:
- names.append(self.args.kwarg)
+ names.append(self.args.kwarg.name)
+ if self.args.keyword_only:
+ names.extend([arg.name for arg in self.keyword_only])
return names
def callable(self):
diff --git a/astroid/tree/treeabc.py b/astroid/tree/treeabc.py
index 6ad96ba8..5c008255 100644
--- a/astroid/tree/treeabc.py
+++ b/astroid/tree/treeabc.py
@@ -67,6 +67,10 @@ class Name(NodeNG):
"""Class representing a Name node"""
+class Parameter(NodeNG):
+ """Class representing a parameter node."""
+
+
class Arguments(NodeNG):
"""Class representing an Arguments node"""
@@ -339,3 +343,12 @@ class ReservedName(NodeNG):
class Unknown(NodeNG):
pass
+
+
+class Empty(NodeNG):
+ """Empty nodes represents the lack of something
+
+ For instance, they can be used to represent missing annotations
+ or defaults for arguments or anything where None is a valid
+ value.
+ """