Repository: numba/numba

Issue:
Double star expansion of keyword arguments in jitclass __init__ regression
In testing Numba 0.27, I noticed the following code now fails, where it previously worked in the 0.26 release:

``` python
import numba as nb

spec = [('a', nb.int64),
        ('b', nb.float64)]

@nb.jitclass(spec)
class TestClass(object):
    def __init__(self, x, y, z):
        self.a = x * y
        self.b = z

kwargs = {'y': 2, 'z': 1.1}
tc = TestClass(2, **kwargs)
```

In 0.27 I now get the following error:

```
Traceback (most recent call last):
  File "test.py", line 14, in <module>
    tc = TestClass(2, **kwargs)
  File "/Users/lev/anaconda/envs/test-numba/lib/python2.7/site-packages/numba/jitclass/base.py", line 99, in __call__
    return cls._ctor(*args, **kwargs)
TypeError: some keyword arguments unexpected
```



Discussion:

Resolving patch:
diff --git a/numba/jitclass/base.py b/numba/jitclass/base.py
--- a/numba/jitclass/base.py
+++ b/numba/jitclass/base.py
@@ -12,7 +12,7 @@
 from numba.typing import templates
 from numba.datamodel import default_manager, models
 from numba.targets import imputils
-from numba import cgutils
+from numba import cgutils, utils
 from numba.six import exec_
 from . import _box
 
@@ -59,11 +59,22 @@ def _mangle_attr(name):
 # Class object
 
 _ctor_template = """
-def ctor(*args):
-    return __numba_cls_(*args)
+def ctor({args}):
+    return __numba_cls_({args})
 """
 
 
+def _getargs(fn):
+    """
+    Returns list of positional and keyword argument names in order. 
+    """
+    sig = utils.pysignature(fn)
+    params = sig.parameters
+    args = [k for k, v in params.items() 
+            if (v.kind & v.POSITIONAL_OR_KEYWORD) == v.POSITIONAL_OR_KEYWORD]
+    return args
+
+
 class JitClassType(type):
     """
     The type of any jitclass.
@@ -84,7 +95,11 @@ def _set_init(cls):
         Generate a wrapper for calling the constructor from pure Python.
         Note the wrapper will only accept positional arguments.
         """
-        ctor_source = _ctor_template
+        init = cls.class_type.instance_type.methods['__init__']
+        # get postitional and keyword arguments
+        # offset by one to exclude the `self` arg
+        args = _getargs(init)[1:]
+        ctor_source = _ctor_template.format(args=', '.join(args))
         glbls = {"__numba_cls_": cls}
         exec_(ctor_source, glbls)
         ctor = glbls['ctor']


Test patch:
diff --git a/numba/tests/test_jitclasses.py b/numba/tests/test_jitclasses.py
--- a/numba/tests/test_jitclasses.py
+++ b/numba/tests/test_jitclasses.py
@@ -584,6 +584,24 @@ def aval(self):
         self.assertEqual(Apple.foo.__doc__, 'foo method docstring')
         self.assertEqual(Apple.aval.__doc__, 'aval property docstring')
 
+    def test_kwargs(self):
+        spec = [('a', int32),
+                ('b', float64)]
+
+        @jitclass(spec)
+        class TestClass(object):
+            def __init__(self, x, y, z):
+                self.a = x * y
+                self.b = z
+
+        x = 2
+        y = 2
+        z = 1.1
+        kwargs = {'y': y, 'z': z}
+        tc = TestClass(x=2, **kwargs)
+        self.assertEqual(tc.a, x * y)
+        self.assertEqual(tc.b, z)
+
 
 if __name__ == '__main__':
     unittest.main()