Skip to content

Commit

Permalink
feat: Support all kinds of functions arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
pawamoy committed Sep 25, 2021
1 parent 8e229aa commit c177562
Show file tree
Hide file tree
Showing 4 changed files with 325 additions and 28 deletions.
42 changes: 40 additions & 2 deletions src/griffe/dataclasses.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,44 @@ def as_dict(self, full=False) -> dict[str, Any]:
}


class Arguments:
"""This class is a container for arguments.
It allows to get arguments using their position (index) or their name.
"""

def __init__(self) -> None:
"""Initialize the arguments container."""
self._arguments_list: list[Argument] = []
self._arguments_dict: dict[str, Argument] = {}

def __getitem__(self, name_or_index: int | str) -> Argument:
if isinstance(name_or_index, int):
return self._arguments_list[name_or_index]
return self._arguments_dict[name_or_index]

def __len__(self):
return len(self._arguments_list)

def __iter__(self):
return iter(self._arguments_list)

def add(self, argument: Argument) -> None:
"""Add an argument to the container.
Arguments:
argument: The function argument to add.
Raises:
ValueError: When an argument with the same name is already present.
"""
if argument.name not in self._arguments_dict:
self._arguments_dict[argument.name] = argument
self._arguments_list.append(argument)
else:
raise ValueError(f"argument {argument.name} already present")


class Kind(enum.Enum):
"""Enumeration of the different objects kinds.
Expand Down Expand Up @@ -450,7 +488,7 @@ class Function(Object):
def __init__(
self,
*args,
arguments: list[Argument] | None = None,
arguments: Arguments | None = None,
returns: str | None = None,
decorators: list[Decorator] | None = None,
**kwargs,
Expand All @@ -465,7 +503,7 @@ def __init__(
**kwargs: See [`griffe.dataclasses.Object`][].
"""
super().__init__(*args, **kwargs)
self.arguments = arguments or []
self.arguments = arguments or Arguments()
self.returns = returns
self.decorators = decorators or []

Expand Down
100 changes: 74 additions & 26 deletions src/griffe/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@

import ast
import inspect
from itertools import zip_longest
from pathlib import Path

from griffe.collections import lines_collection
from griffe.dataclasses import Argument, Class, Decorator, Docstring, Function, Module
from griffe.dataclasses import Argument, Arguments, Class, Decorator, Docstring, Function, Module
from griffe.extensions import Extensions
from griffe.extensions.base import _BaseVisitor # noqa: WPS450

Expand Down Expand Up @@ -52,8 +53,34 @@ def _get_docstring(node):
def _get_base_class_name(node):
if isinstance(node, ast.Attribute):
return f"{_get_base_class_name(node.value)}.{node.attr}"
if isinstance(node, ast.Name):
def _get_annotation(node):
if node is None:
return None
if isinstance(node, Name):
return node.id
if isinstance(node, Constant):
return node.value
if isinstance(node, Attribute):
return f"{_get_annotation(node.value)}.{node.attr}"
if isinstance(node, BinOp) and isinstance(node.op, BitOr):
return f"{_get_annotation(node.left)} | {_get_annotation(node.right)}"
if isinstance(node, Subscript):
return f"{_get_annotation(node.value)}[{_get_annotation(node.slice)}]"
if isinstance(node, Index): # python 3.8
return _get_annotation(node.value)
return None


def _get_argument_default(node, filepath):
if node is None:
return None
if isinstance(node, Constant):
return repr(node.value)
if isinstance(node, Name):
return node.id
if node.lineno == node.end_lineno:
return lines_collection[filepath][node.lineno - 1][node.col_offset : node.end_col_offset]
# TODO: handle multiple line defaults


class _MainVisitor(_BaseVisitor): # noqa: WPS338
Expand Down Expand Up @@ -152,30 +179,51 @@ def visit_FunctionDef(self, node) -> None: # noqa: WPS231
lineno = node.lineno

# handle arguments
arguments = []
for arg in node.args.args:
annotation: str | None
kind = inspect.Parameter.POSITIONAL_OR_KEYWORD
if isinstance(arg.annotation, ast.Name):
annotation = arg.annotation.id
elif isinstance(arg.annotation, ast.Constant):
annotation = arg.annotation.value
elif isinstance(arg.annotation, ast.Attribute):
annotation = arg.annotation.attr
else:
annotation = None
arguments.append(Argument(arg.arg, annotation, kind, None))

# handle arguments defaults
for index, default in enumerate(reversed(node.args.defaults), 1):
if isinstance(default, ast.Constant):
arguments[-index].default = repr(default.value)
elif isinstance(default, ast.Name):
arguments[-index].default = default.id
elif default.lineno == default.end_lineno:
value = lines_collection[self.filepath][default.lineno - 1][default.col_offset : default.end_col_offset]
arguments[-index].default = value
# TODO: handle multiple line defaults
arguments = Arguments()
annotation: str | None

# TODO: probably some optimisations to do here
args_kinds_defaults = reversed(
(
*zip_longest( # noqa: WPS356
reversed(
(
*zip_longest(node.args.posonlyargs, [], fillvalue=inspect.Parameter.POSITIONAL_ONLY),
*zip_longest(node.args.args, [], fillvalue=inspect.Parameter.POSITIONAL_OR_KEYWORD),
),
),
reversed(node.args.defaults),
fillvalue=None,
),
)
)
for (arg, kind), default in args_kinds_defaults:
annotation = _get_annotation(arg.annotation)
default = _get_argument_default(default, self.filepath)
arguments.add(Argument(arg.arg, annotation, kind, default))

if node.args.vararg:
annotation = _get_annotation(node.args.vararg.annotation)
arguments.add(Argument(f"*{node.args.vararg.arg}", annotation, inspect.Parameter.VAR_POSITIONAL, None))

# TODO: probably some optimisations to do here
kwargs_defaults = reversed(
(
*zip_longest( # noqa: WPS356
reversed(node.args.kwonlyargs),
reversed(node.args.kw_defaults),
fillvalue=None,
),
)
)
for kwarg, default in kwargs_defaults: # noqa: WPS440
annotation = _get_annotation(kwarg.annotation)
default = _get_argument_default(default, self.filepath)
arguments.add(Argument(kwarg.arg, annotation, inspect.Parameter.KEYWORD_ONLY, default))

if node.args.kwarg:
annotation = _get_annotation(node.args.kwarg.annotation)
arguments.add(Argument(f"**{node.args.kwarg.arg}", annotation, inspect.Parameter.VAR_KEYWORD, None))

# handle return annotation
if isinstance(node.returns, ast.Constant):
Expand Down
48 changes: 48 additions & 0 deletions tests/fixtures/functions/arguments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

import typing
from typing import Any


def f_posonly(posonly, /):
...


def f_posonly_default(posonly=0, /):
...


def f_posonly_poskw(posonly, /, poskw):
...


def f_posonly_poskw_default(posonly, /, poskw=0):
...


def f_posonly_default_poskw_default(posonly=0, /, poskw=1):
...


def f_posonly_poskw_kwonly(posonly, /, poskw, *, kwonly):
...


def f_posonly_poskw_kwonly_default(posonly, /, poskw, *, kwonly=0):
...


def f_posonly_poskw_default_kwonly_default(posonly, /, poskw=0, *, kwonly=1):
...


def f_posonly_default_poskw_default_kwonly_default(posonly=0, /, poskw=1, *, kwonly=2):
...


def f_var(*args: str, kw=1, **kwargs: int):
...


def f_annorations(a: str, b: Any, c: typing.Optional[typing.List[int]], d: float | None):
...
Loading

0 comments on commit c177562

Please sign in to comment.