Repository: ipython/ipyparallel

Issue:
Required Keyword defaults missing after serialization round trip
When using Keyword-Only arguments in a function definition, the defaults for those keywords are lost on a round trip through the `pack/unpack_apply_message` functions of the `serialize` module.

Minimal example in a file called "stest.py":

```python
from ipyparallel.serialize import pack_apply_message, unpack_apply_message

def compute(a, *, b = True):
  return "the_result (" + b.__repr__() + ")"

m = pack_apply_message(compute, [1], {})

f, a, p = unpack_apply_message(m)

r = f(*a, **p)

print("Result is: {}".format(r))
```

yields the following error:

```
  File "stest.py", line 10, in <module>
    r = f(*a, **p)
TypeError: compute() missing 1 required keyword-only argument: 'b'
```

Expected behavior:

The unpacked function should also preserve any defaults for its Keyword-Only arguments (I think its the `__kwdefaults__` attribute of the function).

Other details:

Type annotations don't survive round trip sterilization either, but those don't cause a failure.

xref issue: Parsl/parsl#1166

Potential solution which makes the minimal example work from @benclifford: https://github.com/benclifford/ipyparallel/commit/e028bf2feff4060eb5ba4b718fddb81d0ccefcfe

Comments:

Patch:
diff --git a/ipyparallel/client/client.py b/ipyparallel/client/client.py
index 5b688e16..36b4b402 100644
--- a/ipyparallel/client/client.py
+++ b/ipyparallel/client/client.py
@@ -1325,6 +1325,8 @@ class Client(HasTraits):
         if jobs is None:
             # get futures for results
             futures = [f for f in self._futures.values() if hasattr(f, 'output')]
+            if not futures:
+                return
             ar = AsyncResult(self, futures, owner=False)
         else:
             ar = self._asyncresult_from_jobs(jobs, owner=False)
diff --git a/ipyparallel/serialize/canning.py b/ipyparallel/serialize/canning.py
index f2ba4140..b83ef98e 100644
--- a/ipyparallel/serialize/canning.py
+++ b/ipyparallel/serialize/canning.py
@@ -193,6 +193,16 @@ class CannedFunction(CannedObject):
         else:
             self.defaults = None
 
+        if f.__kwdefaults__:
+            self.kwdefaults = can_dict(f.__kwdefaults__)
+        else:
+            self.kwdefaults = None
+
+        if f.__annotations__:
+            self.annotations = can_dict(f.__annotations__)
+        else:
+            self.annotations = None
+
         closure = py3compat.get_closure(f)
         if closure:
             self.closure = tuple(can(cell) for cell in closure)
@@ -218,11 +228,25 @@ class CannedFunction(CannedObject):
             defaults = tuple(uncan(cfd, g) for cfd in self.defaults)
         else:
             defaults = None
+
+        if self.kwdefaults:
+            kwdefaults = uncan_dict(self.kwdefaults)
+        else:
+            kwdefaults = None
+        if self.annotations:
+            annotations = uncan_dict(self.annotations)
+        else:
+            annotations = {}
+
         if self.closure:
             closure = tuple(uncan(cell, g) for cell in self.closure)
         else:
             closure = None
         newFunc = FunctionType(self.code, g, self.__name__, defaults, closure)
+        if kwdefaults:
+            newFunc.__kwdefaults__ = kwdefaults
+        if annotations:
+            newFunc.__annotations__ = annotations
         return newFunc
 
 


Test patch:
diff --git a/ipyparallel/tests/test_canning.py b/ipyparallel/tests/test_canning.py
index f462b343..090cb837 100644
--- a/ipyparallel/tests/test_canning.py
+++ b/ipyparallel/tests/test_canning.py
@@ -21,6 +21,10 @@ def loads(obj):
     return uncan(pickle.loads(obj))
 
 
+def roundtrip(obj):
+    return loads(dumps(obj))
+
+
 def test_no_closure():
     @interactive
     def foo():
@@ -109,3 +113,20 @@ def test_can_partial_buffers():
     loaded.buffers = buffers
     pfoo2 = uncan(loaded)
     assert pfoo2() == partial_foo()
+
+
+def test_keyword_only_arguments():
+    def compute(a, *, b=3):
+        return a * b
+
+    assert compute(2) == 6
+    compute_2 = roundtrip(compute)
+    assert compute_2(2) == 6
+
+
+def test_annotations():
+    def f(a: int):
+        return a
+
+    f2 = roundtrip(f)
+    assert f2.__annotations__ == f.__annotations__