Skip to content

Commit 4d03bec

Browse files
authored
Fix Arguments.arguments so it actually returns all arguments (#2240)
Arguments.arguments() has been modified so that it returns all arguments as it should (according to its own doc). A test case was also added to verify this. Methods which counted on arguments() not containing vararg and kwonlyargs have also been modified so they work with this new change. Provide more info on *args and **kwargs: Iif available, these nodes (accessed through arguments()) now contain lineno and col offset information. find_argname now works when requesting vararg or kwargs. Closes #2213.
1 parent 5149910 commit 4d03bec

File tree

7 files changed

+127
-29
lines changed

7 files changed

+127
-29
lines changed

ChangeLog

+5
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ What's New in astroid 3.0.0?
66
=============================
77
Release date: TBA
88

9+
* Return all existing arguments when calling ``Arguments.arguments()``. This also means ``find_argname`` will now
10+
use the whole list of arguments for its search.
11+
12+
Closes #2213
13+
914
* Add support for Python 3.12, including PEP 695 type parameter syntax.
1015

1116
Closes #2201

astroid/arguments.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,13 @@ def infer_argument(
181181

182182
positional = self.positional_arguments[: len(funcnode.args.args)]
183183
vararg = self.positional_arguments[len(funcnode.args.args) :]
184-
argindex = funcnode.args.find_argname(name)[0]
184+
185+
# preserving previous behavior, when vararg and kwarg were not included in find_argname results
186+
if name in [funcnode.args.vararg, funcnode.args.kwarg]:
187+
argindex = None
188+
else:
189+
argindex = funcnode.args.find_argname(name)[0]
190+
185191
kwonlyargs = {arg.name for arg in funcnode.args.kwonlyargs}
186192
kwargs = {
187193
key: value

astroid/nodes/node_classes.py

+49-15
Original file line numberDiff line numberDiff line change
@@ -605,7 +605,9 @@ def _infer(
605605
DEPRECATED_ARGUMENT_DEFAULT = "DEPRECATED_ARGUMENT_DEFAULT"
606606

607607

608-
class Arguments(_base_nodes.AssignTypeNode):
608+
class Arguments(
609+
_base_nodes.AssignTypeNode
610+
): # pylint: disable=too-many-instance-attributes
609611
"""Class representing an :class:`ast.arguments` node.
610612
611613
An :class:`Arguments` node represents that arguments in a
@@ -704,7 +706,20 @@ class Arguments(_base_nodes.AssignTypeNode):
704706
kwargannotation: NodeNG | None
705707
"""The type annotation for the variable length keyword arguments."""
706708

707-
def __init__(self, vararg: str | None, kwarg: str | None, parent: NodeNG) -> None:
709+
vararg_node: AssignName | None
710+
"""The node for variable length arguments"""
711+
712+
kwarg_node: AssignName | None
713+
"""The node for variable keyword arguments"""
714+
715+
def __init__(
716+
self,
717+
vararg: str | None,
718+
kwarg: str | None,
719+
parent: NodeNG,
720+
vararg_node: AssignName | None = None,
721+
kwarg_node: AssignName | None = None,
722+
) -> None:
708723
"""Almost all attributes can be None for living objects where introspection failed."""
709724
super().__init__(
710725
parent=parent,
@@ -720,6 +735,9 @@ def __init__(self, vararg: str | None, kwarg: str | None, parent: NodeNG) -> Non
720735
self.kwarg = kwarg
721736
"""The name of the variable length keyword arguments."""
722737

738+
self.vararg_node = vararg_node
739+
self.kwarg_node = kwarg_node
740+
723741
# pylint: disable=too-many-arguments
724742
def postinit(
725743
self,
@@ -780,8 +798,21 @@ def fromlineno(self) -> int:
780798

781799
@cached_property
782800
def arguments(self):
783-
"""Get all the arguments for this node, including positional only and positional and keyword"""
784-
return list(itertools.chain((self.posonlyargs or ()), self.args or ()))
801+
"""Get all the arguments for this node. This includes:
802+
* Positional only arguments
803+
* Positional arguments
804+
* Keyword arguments
805+
* Variable arguments (.e.g *args)
806+
* Variable keyword arguments (e.g **kwargs)
807+
"""
808+
retval = list(itertools.chain((self.posonlyargs or ()), (self.args or ())))
809+
if self.vararg_node:
810+
retval.append(self.vararg_node)
811+
retval += self.kwonlyargs or ()
812+
if self.kwarg_node:
813+
retval.append(self.kwarg_node)
814+
815+
return retval
785816

786817
def format_args(self, *, skippable_names: set[str] | None = None) -> str:
787818
"""Get the arguments formatted as string.
@@ -911,15 +942,20 @@ def default_value(self, argname):
911942
:raises NoDefault: If there is no default value defined for the
912943
given argument.
913944
"""
914-
args = self.arguments
945+
args = [
946+
arg for arg in self.arguments if arg.name not in [self.vararg, self.kwarg]
947+
]
948+
949+
index = _find_arg(argname, self.kwonlyargs)[0]
950+
if index is not None and self.kw_defaults[index] is not None:
951+
return self.kw_defaults[index]
952+
915953
index = _find_arg(argname, args)[0]
916954
if index is not None:
917-
idx = index - (len(args) - len(self.defaults))
955+
idx = index - (len(args) - len(self.defaults) - len(self.kw_defaults))
918956
if idx >= 0:
919957
return self.defaults[idx]
920-
index = _find_arg(argname, self.kwonlyargs)[0]
921-
if index is not None and self.kw_defaults[index] is not None:
922-
return self.kw_defaults[index]
958+
923959
raise NoDefault(func=self.parent, name=argname)
924960

925961
def is_argument(self, name) -> bool:
@@ -934,11 +970,7 @@ def is_argument(self, name) -> bool:
934970
return True
935971
if name == self.kwarg:
936972
return True
937-
return (
938-
self.find_argname(name)[1] is not None
939-
or self.kwonlyargs
940-
and _find_arg(name, self.kwonlyargs)[1] is not None
941-
)
973+
return self.find_argname(name)[1] is not None
942974

943975
def find_argname(self, argname, rec=DEPRECATED_ARGUMENT_DEFAULT):
944976
"""Get the index and :class:`AssignName` node for given name.
@@ -956,7 +988,9 @@ def find_argname(self, argname, rec=DEPRECATED_ARGUMENT_DEFAULT):
956988
stacklevel=2,
957989
)
958990
if self.arguments:
959-
return _find_arg(argname, self.arguments)
991+
index, argument = _find_arg(argname, self.arguments)
992+
if argument:
993+
return index, argument
960994
return None, None
961995

962996
def get_children(self):

astroid/nodes/scoped_nodes/scoped_nodes.py

+2-10
Original file line numberDiff line numberDiff line change
@@ -963,11 +963,7 @@ def argnames(self) -> list[str]:
963963
names = [elt.name for elt in self.args.arguments]
964964
else:
965965
names = []
966-
if self.args.vararg:
967-
names.append(self.args.vararg)
968-
names += [elt.name for elt in self.args.kwonlyargs]
969-
if self.args.kwarg:
970-
names.append(self.args.kwarg)
966+
971967
return names
972968

973969
def infer_call_result(
@@ -1280,11 +1276,7 @@ def argnames(self) -> list[str]:
12801276
names = [elt.name for elt in self.args.arguments]
12811277
else:
12821278
names = []
1283-
if self.args.vararg:
1284-
names.append(self.args.vararg)
1285-
names += [elt.name for elt in self.args.kwonlyargs]
1286-
if self.args.kwarg:
1287-
names.append(self.args.kwarg)
1279+
12881280
return names
12891281

12901282
def getattr(

astroid/protocols.py

+4-3
Original file line numberDiff line numberDiff line change
@@ -352,14 +352,15 @@ def _arguments_infer_argname(
352352
# more
353353
from astroid import arguments # pylint: disable=import-outside-toplevel
354354

355-
if not (self.arguments or self.vararg or self.kwarg):
355+
if not self.arguments:
356356
yield util.Uninferable
357357
return
358358

359+
args = [arg for arg in self.arguments if arg.name not in [self.vararg, self.kwarg]]
359360
functype = self.parent.type
360361
# first argument of instance/class method
361362
if (
362-
self.arguments
363+
args
363364
and getattr(self.arguments[0], "name", None) == name
364365
and functype != "staticmethod"
365366
):
@@ -388,7 +389,7 @@ def _arguments_infer_argname(
388389
if name == self.vararg:
389390
vararg = nodes.const_factory(())
390391
vararg.parent = self
391-
if not self.arguments and self.parent.name == "__init__":
392+
if not args and self.parent.name == "__init__":
392393
cls = self.parent.parent.scope()
393394
vararg.elts = [cls.instantiate_class()]
394395
yield vararg

astroid/rebuilder.py

+24
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from astroid.const import IS_PYPY, PY38, PY39_PLUS, PY312_PLUS, Context
2222
from astroid.manager import AstroidManager
2323
from astroid.nodes import NodeNG
24+
from astroid.nodes.node_classes import AssignName
2425
from astroid.nodes.utils import Position
2526
from astroid.typing import InferenceResult
2627

@@ -561,10 +562,33 @@ def visit_arguments(self, node: ast.arguments, parent: NodeNG) -> nodes.Argument
561562
"""Visit an Arguments node by returning a fresh instance of it."""
562563
vararg: str | None = None
563564
kwarg: str | None = None
565+
vararg_node = node.vararg
566+
kwarg_node = node.kwarg
567+
564568
newnode = nodes.Arguments(
565569
node.vararg.arg if node.vararg else None,
566570
node.kwarg.arg if node.kwarg else None,
567571
parent,
572+
AssignName(
573+
vararg_node.arg,
574+
vararg_node.lineno,
575+
vararg_node.col_offset,
576+
parent,
577+
end_lineno=vararg_node.end_lineno,
578+
end_col_offset=vararg_node.end_col_offset,
579+
)
580+
if vararg_node
581+
else None,
582+
AssignName(
583+
kwarg_node.arg,
584+
kwarg_node.lineno,
585+
kwarg_node.col_offset,
586+
parent,
587+
end_lineno=kwarg_node.end_lineno,
588+
end_col_offset=kwarg_node.end_col_offset,
589+
)
590+
if kwarg_node
591+
else None,
568592
)
569593
args = [self.visit(child, newnode) for child in node.args]
570594
defaults = [self.visit(child, newnode) for child in node.defaults]

tests/test_nodes.py

+36
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
Uninferable,
2323
bases,
2424
builder,
25+
extract_node,
2526
nodes,
2627
parse,
2728
test_utils,
@@ -1975,3 +1976,38 @@ def test_str_repr_no_warnings(node):
19751976
test_node = node(**args)
19761977
str(test_node)
19771978
repr(test_node)
1979+
1980+
1981+
def test_arguments_contains_all():
1982+
"""Ensure Arguments.arguments actually returns all available arguments"""
1983+
1984+
def manually_get_args(arg_node) -> set:
1985+
names = set()
1986+
if arg_node.args.vararg:
1987+
names.add(arg_node.args.vararg)
1988+
if arg_node.args.kwarg:
1989+
names.add(arg_node.args.kwarg)
1990+
1991+
names.update([x.name for x in arg_node.args.args])
1992+
names.update([x.name for x in arg_node.args.kwonlyargs])
1993+
1994+
return names
1995+
1996+
node = extract_node("""def a(fruit: str, *args, b=None, c=None, **kwargs): ...""")
1997+
assert manually_get_args(node) == {x.name for x in node.args.arguments}
1998+
1999+
node = extract_node("""def a(mango: int, b="banana", c=None, **kwargs): ...""")
2000+
assert manually_get_args(node) == {x.name for x in node.args.arguments}
2001+
2002+
node = extract_node("""def a(self, num = 10, *args): ...""")
2003+
assert manually_get_args(node) == {x.name for x in node.args.arguments}
2004+
2005+
2006+
def test_arguments_default_value():
2007+
node = extract_node(
2008+
"def fruit(eat='please', *, peel='no', trim='yes', **kwargs): ..."
2009+
)
2010+
assert node.args.default_value("eat").value == "please"
2011+
2012+
node = extract_node("def fruit(seeds, flavor='good', *, peel='maybe'): ...")
2013+
assert node.args.default_value("flavor").value == "good"

0 commit comments

Comments
 (0)