Repository: ibis-project/ibis

Issue:
bug: Wrong parameter order in `AnnotableMeta`. 
The following code which was working as of 3ce4f2a8 is now no longer working. Reasonably sure it is because of the changes in https://github.com/ibis-project/ibis/commit/088169ae96c476e0363baaa1fa5f116be5d7aae6 around constructing the `__signature__` in `AnnotableMeta` (https://github.com/ibis-project/ibis/commit/088169ae96c476e0363baaa1fa5f116be5d7aae6#diff-1ba248744f86eb25b4e863648c19353294f34c47b637d1fe88d66eccfc1e1119R100-R108). 

```python
import ibis.expr.operations as ops
import ibis.expr.rules as _rlz
import ibis.expr.types as ir

class SomeOp(ops.ValueOp):
    a = _rlz.instance_of(ir.TableExpr)
    b = _rlz.optional(_rlz.instance_of(ir.TableExpr), default=None)
    c = _rlz.instance_of(bool)
```
now raises 
```
Traceback (most recent call last):
  File "/home/gmanoim/tmp/tmp.py", line 5, in <module>
    class SomeOp(ops.ValueOp):
  File "../ibis/common/grounds.py", line 116, in __new__
    signature = inspect.Signature(
  File "../python3.9/inspect.py", line 2817, in __init__
    raise ValueError(msg)
ValueError: non-default argument follows default argument
```



Discussion:
I believe this is intentional so that we conform to Python's signature restrictions. @kszucs would know more.

Resolving patch:
diff --git a/ibis/common/grounds.py b/ibis/common/grounds.py
--- a/ibis/common/grounds.py
+++ b/ibis/common/grounds.py
@@ -103,18 +103,23 @@ def __new__(metacls, clsname, bases, dct):
 
         # mandatory fields without default values must preceed the optional
         # ones in the function signature, the partial ordering will be kept
-        new_params, inherited_params, optional_inherited_params = [], [], []
+        new_args, new_kwargs = [], []
+        inherited_args, inherited_kwargs = [], []
+
         for name, param in params.items():
             if name in inherited:
                 if param.default is EMPTY:
-                    inherited_params.append(param)
+                    inherited_args.append(param)
                 else:
-                    optional_inherited_params.append(param)
+                    inherited_kwargs.append(param)
             else:
-                new_params.append(param)
+                if param.default is EMPTY:
+                    new_args.append(param)
+                else:
+                    new_kwargs.append(param)
 
         signature = inspect.Signature(
-            inherited_params + new_params + optional_inherited_params
+            inherited_args + new_args + new_kwargs + inherited_kwargs
         )
 
         attribs["__slots__"] = tuple(slots)


Test patch:
diff --git a/ibis/common/tests/test_grounds.py b/ibis/common/tests/test_grounds.py
--- a/ibis/common/tests/test_grounds.py
+++ b/ibis/common/tests/test_grounds.py
@@ -192,6 +192,28 @@ class NoHooves(Farm):
     assert g1.goats == 0
 
 
+def test_keyword_argument_reordering():
+    class Alpha(Annotable):
+        a = IsInt
+        b = IsInt
+
+    class Beta(Alpha):
+        c = IsInt
+        d = Optional(IsInt, default=0)
+        e = IsInt
+
+    obj = Beta(1, 2, 3, 4)
+    assert obj.a == 1
+    assert obj.b == 2
+    assert obj.c == 3
+    assert obj.e == 4
+    assert obj.d == 0
+
+    obj = Beta(1, 2, 3, 4, 5)
+    assert obj.d == 5
+    assert obj.e == 4
+
+
 def test_not_copy_default():
     default = tuple()