Skip to content

Fix Arguments.arguments so it actually returns all arguments #2240

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ What's New in astroid 3.0.0?
=============================
Release date: TBA

* Return all existing arguments when calling ``Arguments.arguments()``. This also means ``find_argname`` will now
use the whole list of arguments for its search.

Closes #2213

* Add support for Python 3.12, including PEP 695 type parameter syntax.

Closes #2201
Expand Down
8 changes: 7 additions & 1 deletion astroid/arguments.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,13 @@ def infer_argument(

positional = self.positional_arguments[: len(funcnode.args.args)]
vararg = self.positional_arguments[len(funcnode.args.args) :]
argindex = funcnode.args.find_argname(name)[0]

# 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
Expand Down
64 changes: 49 additions & 15 deletions astroid/nodes/node_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,9 @@ def _infer(
DEPRECATED_ARGUMENT_DEFAULT = "DEPRECATED_ARGUMENT_DEFAULT"


class Arguments(_base_nodes.AssignTypeNode):
class Arguments(
_base_nodes.AssignTypeNode
): # pylint: disable=too-many-instance-attributes
"""Class representing an :class:`ast.arguments` node.

An :class:`Arguments` node represents that arguments in a
Expand Down Expand Up @@ -704,7 +706,20 @@ class Arguments(_base_nodes.AssignTypeNode):
kwargannotation: NodeNG | None
"""The type annotation for the variable length keyword arguments."""

def __init__(self, vararg: str | None, kwarg: str | None, parent: NodeNG) -> None:
vararg_node: AssignName | None
"""The node for variable length arguments"""

kwarg_node: AssignName | None
"""The node for variable keyword arguments"""

def __init__(
self,
vararg: str | None,
kwarg: str | None,
parent: NodeNG,
vararg_node: AssignName | None = None,
kwarg_node: AssignName | None = None,
) -> None:
"""Almost all attributes can be None for living objects where introspection failed."""
super().__init__(
parent=parent,
Expand All @@ -720,6 +735,9 @@ def __init__(self, vararg: str | None, kwarg: str | None, parent: NodeNG) -> Non
self.kwarg = kwarg
"""The name of the variable length keyword arguments."""

self.vararg_node = vararg_node
self.kwarg_node = kwarg_node

# pylint: disable=too-many-arguments
def postinit(
self,
Expand Down Expand Up @@ -780,8 +798,21 @@ def fromlineno(self) -> int:

@cached_property
def arguments(self):
"""Get all the arguments for this node, including positional only and positional and keyword"""
return list(itertools.chain((self.posonlyargs or ()), self.args or ()))
"""Get all the arguments for this node. This includes:
* Positional only arguments
* Positional arguments
* Keyword arguments
* Variable arguments (.e.g *args)
* Variable keyword arguments (e.g **kwargs)
"""
retval = list(itertools.chain((self.posonlyargs or ()), (self.args or ())))
if self.vararg_node:
retval.append(self.vararg_node)
retval += self.kwonlyargs or ()
if self.kwarg_node:
retval.append(self.kwarg_node)

return retval

def format_args(self, *, skippable_names: set[str] | None = None) -> str:
"""Get the arguments formatted as string.
Expand Down Expand Up @@ -911,15 +942,20 @@ def default_value(self, argname):
:raises NoDefault: If there is no default value defined for the
given argument.
"""
args = self.arguments
args = [
arg for arg in self.arguments if arg.name not in [self.vararg, self.kwarg]
]

index = _find_arg(argname, self.kwonlyargs)[0]
if index is not None and self.kw_defaults[index] is not None:
return self.kw_defaults[index]

index = _find_arg(argname, args)[0]
if index is not None:
idx = index - (len(args) - len(self.defaults))
idx = index - (len(args) - len(self.defaults) - len(self.kw_defaults))
if idx >= 0:
return self.defaults[idx]
index = _find_arg(argname, self.kwonlyargs)[0]
if index is not None and self.kw_defaults[index] is not None:
return self.kw_defaults[index]

raise NoDefault(func=self.parent, name=argname)

def is_argument(self, name) -> bool:
Expand All @@ -934,11 +970,7 @@ def is_argument(self, name) -> bool:
return True
if name == self.kwarg:
return True
return (
self.find_argname(name)[1] is not None
or self.kwonlyargs
and _find_arg(name, self.kwonlyargs)[1] is not None
)
return self.find_argname(name)[1] is not None

def find_argname(self, argname, rec=DEPRECATED_ARGUMENT_DEFAULT):
"""Get the index and :class:`AssignName` node for given name.
Expand All @@ -956,7 +988,9 @@ def find_argname(self, argname, rec=DEPRECATED_ARGUMENT_DEFAULT):
stacklevel=2,
)
if self.arguments:
return _find_arg(argname, self.arguments)
index, argument = _find_arg(argname, self.arguments)
if argument:
return index, argument
return None, None

def get_children(self):
Expand Down
12 changes: 2 additions & 10 deletions astroid/nodes/scoped_nodes/scoped_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -963,11 +963,7 @@ def argnames(self) -> list[str]:
names = [elt.name for elt in self.args.arguments]
else:
names = []
if self.args.vararg:
names.append(self.args.vararg)
names += [elt.name for elt in self.args.kwonlyargs]
if self.args.kwarg:
names.append(self.args.kwarg)

return names

def infer_call_result(
Expand Down Expand Up @@ -1280,11 +1276,7 @@ def argnames(self) -> list[str]:
names = [elt.name for elt in self.args.arguments]
else:
names = []
if self.args.vararg:
names.append(self.args.vararg)
names += [elt.name for elt in self.args.kwonlyargs]
if self.args.kwarg:
names.append(self.args.kwarg)

return names

def getattr(
Expand Down
7 changes: 4 additions & 3 deletions astroid/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -352,14 +352,15 @@ def _arguments_infer_argname(
# more
from astroid import arguments # pylint: disable=import-outside-toplevel

if not (self.arguments or self.vararg or self.kwarg):
if not self.arguments:
yield util.Uninferable
return

args = [arg for arg in self.arguments if arg.name not in [self.vararg, self.kwarg]]
functype = self.parent.type
# first argument of instance/class method
if (
self.arguments
args
and getattr(self.arguments[0], "name", None) == name
and functype != "staticmethod"
):
Expand Down Expand Up @@ -388,7 +389,7 @@ def _arguments_infer_argname(
if name == self.vararg:
vararg = nodes.const_factory(())
vararg.parent = self
if not self.arguments and self.parent.name == "__init__":
if not args and self.parent.name == "__init__":
cls = self.parent.parent.scope()
vararg.elts = [cls.instantiate_class()]
yield vararg
Expand Down
24 changes: 24 additions & 0 deletions astroid/rebuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from astroid.const import IS_PYPY, PY38, PY39_PLUS, PY312_PLUS, Context
from astroid.manager import AstroidManager
from astroid.nodes import NodeNG
from astroid.nodes.node_classes import AssignName
from astroid.nodes.utils import Position
from astroid.typing import InferenceResult

Expand Down Expand Up @@ -561,10 +562,33 @@ def visit_arguments(self, node: ast.arguments, parent: NodeNG) -> nodes.Argument
"""Visit an Arguments node by returning a fresh instance of it."""
vararg: str | None = None
kwarg: str | None = None
vararg_node = node.vararg
kwarg_node = node.kwarg

newnode = nodes.Arguments(
node.vararg.arg if node.vararg else None,
node.kwarg.arg if node.kwarg else None,
parent,
AssignName(
vararg_node.arg,
vararg_node.lineno,
vararg_node.col_offset,
parent,
end_lineno=vararg_node.end_lineno,
end_col_offset=vararg_node.end_col_offset,
)
if vararg_node
else None,
AssignName(
kwarg_node.arg,
kwarg_node.lineno,
kwarg_node.col_offset,
parent,
end_lineno=kwarg_node.end_lineno,
end_col_offset=kwarg_node.end_col_offset,
)
if kwarg_node
else None,
)
args = [self.visit(child, newnode) for child in node.args]
defaults = [self.visit(child, newnode) for child in node.defaults]
Expand Down
36 changes: 36 additions & 0 deletions tests/test_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
Uninferable,
bases,
builder,
extract_node,
nodes,
parse,
test_utils,
Expand Down Expand Up @@ -1975,3 +1976,38 @@ def test_str_repr_no_warnings(node):
test_node = node(**args)
str(test_node)
repr(test_node)


def test_arguments_contains_all():
"""Ensure Arguments.arguments actually returns all available arguments"""

def manually_get_args(arg_node) -> set:
names = set()
if arg_node.args.vararg:
names.add(arg_node.args.vararg)
if arg_node.args.kwarg:
names.add(arg_node.args.kwarg)

names.update([x.name for x in arg_node.args.args])
names.update([x.name for x in arg_node.args.kwonlyargs])

return names

node = extract_node("""def a(fruit: str, *args, b=None, c=None, **kwargs): ...""")
assert manually_get_args(node) == {x.name for x in node.args.arguments}

node = extract_node("""def a(mango: int, b="banana", c=None, **kwargs): ...""")
assert manually_get_args(node) == {x.name for x in node.args.arguments}

node = extract_node("""def a(self, num = 10, *args): ...""")
assert manually_get_args(node) == {x.name for x in node.args.arguments}


def test_arguments_default_value():
node = extract_node(
"def fruit(eat='please', *, peel='no', trim='yes', **kwargs): ..."
)
assert node.args.default_value("eat").value == "please"

node = extract_node("def fruit(seeds, flavor='good', *, peel='maybe'): ...")
assert node.args.default_value("flavor").value == "good"