Repository: scikit-hep/awkward

Issue:
Documentation is missing keyword-only arguments
(And if we had any functions with position-only arguments, they would be missing, too.)

These three lines:

https://github.com/scikit-hep/awkward/blob/8d7a2c31f1ea24cef8a909fb12bb14eb812477ac/docs/prepare_docstrings.py#L146-L148

should become

```python
        argnames = [x.arg for x in node.args.posonlyargs + node.args.args + node.args.kwonlyargs]
        defaults = ["=" + tostr(x) for x in node.args.defaults + node.args.kw_defaults]
        defaults = [""] * (len(argnames) - len(defaults)) + defaults
```

As seen in this AST dump, that's where the other arguments are hiding:

```python
>>> print(ast.dump(ast.parse("def f(a, /, b, c=1, *, d=2): pass"), indent=4))
Module(
    body=[
        FunctionDef(
            name='f',
            args=arguments(
                posonlyargs=[
                    arg(arg='a')],
                args=[
                    arg(arg='b'),
                    arg(arg='c')],
                kwonlyargs=[
                    arg(arg='d')],
                kw_defaults=[
                    Constant(value=2)],
                defaults=[
                    Constant(value=1)]),
            body=[
                Pass()],
            decorator_list=[])],
    type_ignores=[])
```

----------------------

Is the above block the _only_ code that needs to be changed? The handling of lambda expressions might get the same treatment:

https://github.com/scikit-hep/awkward/blob/8d7a2c31f1ea24cef8a909fb12bb14eb812477ac/docs/prepare_docstrings.py#L86-L89

but there wouldn't be any lambda expressions in our documentation. That's only there for completeness.

Similarly, we don't actually have expressions in our documentation, but if we did, call would be missing keywords:

https://github.com/scikit-hep/awkward/blob/8d7a2c31f1ea24cef8a909fb12bb14eb812477ac/docs/prepare_docstrings.py#L61-L64

For reference, here is how the `Lambda` and `Call` AST nodes are formatted:

```python
>>> print(ast.dump(ast.parse("lambda a, /, b, c=1, *, d=2: None"), indent=4))
Module(
    body=[
        Expr(
            value=Lambda(
                args=arguments(
                    posonlyargs=[
                        arg(arg='a')],
                    args=[
                        arg(arg='b'),
                        arg(arg='c')],
                    kwonlyargs=[
                        arg(arg='d')],
                    kw_defaults=[
                        Constant(value=2)],
                    defaults=[
                        Constant(value=1)]),
                body=Constant(value=None)))],
    type_ignores=[])
```

and

```python
>>> print(ast.dump(ast.parse("f(a, b=1, *args, **kwargs)"), indent=4))
Module(
    body=[
        Expr(
            value=Call(
                func=Name(id='f', ctx=Load()),
                args=[
                    Name(id='a', ctx=Load()),
                    Starred(
                        value=Name(id='args', ctx=Load()),
                        ctx=Load())],
                keywords=[
                    keyword(
                        arg='b',
                        value=Constant(value=1)),
                    keyword(
                        value=Name(id='kwargs', ctx=Load()))]))],
    type_ignores=[])
```

But I think only the function signatures (in the `dosig` function) need to be addressed.

(This, by the way, is a byproduct of the project starting in Python 2—no position-only or keyword-only arguments—and ReadTheDocs, which couldn't compile the C++ part and therefore couldn't actually `import awkward`, but it could compile the Python and therefore examine its AST. It could be resolved, in the long run, by replacing this now-unnecessary AST pass, but the above is a very quick fix to the current problem.)


Discussion:

Resolving patch:
diff --git a/docs/prepare_docstrings.py b/docs/prepare_docstrings.py
--- a/docs/prepare_docstrings.py
+++ b/docs/prepare_docstrings.py
@@ -143,10 +143,20 @@ def dosig(node):
     if node is None:
         return "self"
     else:
-        argnames = [x.arg for x in node.args.args]
-        defaults = ["=" + tostr(x) for x in node.args.defaults]
+        non_keyword_args = node.args.posonlyargs + node.args.args
+        argnames = [x.arg for x in non_keyword_args + node.args.kwonlyargs]
+        defaults = [
+            "=" + tostr(x)
+            for x in node.args.defaults + node.args.kw_defaults
+            if x is not None
+        ]
         defaults = [""] * (len(argnames) - len(defaults)) + defaults
-        return ", ".join(x + y for x, y in zip(argnames, defaults))
+        rendered = [x + y for x, y in zip(argnames, defaults)]
+        if node.args.vararg is not None:
+            rendered.insert(len(non_keyword_args), f"*{node.args.vararg.arg}")
+        elif node.args.kwonlyargs:
+            rendered.insert(len(non_keyword_args), "*")
+        return ", ".join(rendered)
 
 
 def dodoc(docstring, qualname, names):
@@ -324,6 +334,56 @@ def dofunction(link, linelink, shortname, name, astfcn):
             dofunction(link, linelink, shortname, toplevel.name, toplevel)
 
 
+def test_signature_pos_or_kw():
+    mod = ast.parse(
+        """
+def func(x, z):
+    ...
+"""
+    )
+    node = mod.body[0]
+    assert dosig(node) == "x, z"
+
+
+def test_signature_kwarg():
+    mod = ast.parse(
+        """
+def func(x, z=None):
+    ...
+"""
+    )
+    node = mod.body[0]
+    assert dosig(node) == "x, z=None"
+
+
+def test_signature_vararg():
+    mod = ast.parse(
+        """
+def func(x, *y, z=None):
+    ...
+"""
+    )
+    node = mod.body[0]
+    assert dosig(node) == "x, *y, z=None"
+
+
+def test_signature_kwonly():
+    mod = ast.parse(
+        """
+def func(x, *, y, z=None):
+    ...
+"""
+    )
+    node = mod.body[0]
+    assert dosig(node) == "x, *, y, z=None"
+
+
+test_signature_pos_or_kw()
+test_signature_kwarg()
+test_signature_vararg()
+test_signature_kwonly()
+
+
 toctree_path = reference_path / "toctree.txt"
 toctree_contents = toctree_path.read_text()
 


Test patch: