Repository: Parsl/parsl

Issue:
Pandas DataFrames as Default Values
Parsl does not work with Pandas DataFrames as the default value to a keyword argument.

For example,

```python
data = pd.DataFrame({'x': [None,2,[3]]})
@App('python', dfk)
def f(d=data):
    return d.sum()
f().result()
```
yields an error

```
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-23-cba3cd6e0846> in <module>()
      3 def f(d=data):
      4     return d.sum()
----> 5 f().result()

~/miniconda3/lib/python3.6/site-packages/parsl/app/app_factory.py in __call__(self, *args, **kwargs)
     73                                  walltime=self.walltime,
     74                                  cache=self.cache,
---> 75                                  fn_hash=self.func_hash)
     76         return app_obj(*args, **kwargs)
     77 

~/miniconda3/lib/python3.6/site-packages/parsl/app/python_app.py in __init__(self, func, executor, walltime, cache, sites, fn_hash)
     16         This bit is the same for both bash & python apps.
     17         """
---> 18         super().__init__(func, executor, walltime=walltime, sites=sites, exec_type="python")
     19         self.fn_hash = fn_hash
     20         self.cache = cache

~/miniconda3/lib/python3.6/site-packages/parsl/app/app.py in __init__(self, func, executor, walltime, sites, cache, exec_type)
     47         self.kwargs = {}
     48         for s in sig.parameters:
---> 49             if sig.parameters[s].default != Parameter.empty:
     50                 self.kwargs[s] = sig.parameters[s].default
     51 

~/miniconda3/lib/python3.6/site-packages/pandas/core/generic.py in __nonzero__(self)
   1119         raise ValueError("The truth value of a {0} is ambiguous. "
   1120                          "Use a.empty, a.bool(), a.item(), a.any() or a.all()."
-> 1121                          .format(self.__class__.__name__))
   1122 
   1123     __bool__ = __nonzero__

ValueError: The truth value of a DataFrame is ambiguous. Use a.empty, a.bool(), a.item(), a.any() or a.all().```


Discussion:
Thanks for the report @WardLT! Just to clarify, do you need to set the default dynamically, or would something like this work:

```
@App('python', dfk)
def f(d=None):
    if data is None:
        data = pd.DataFrame({'x': [None,2,[3]]})
    return d.sum()
f().result()
```
There is a nice discussion [here](http://docs.python-guide.org/en/latest/writing/gotchas/) on why you might want to avoid mutable default arguments to a function. Let me know what your requirements are and we will go from there.
Yea, my use mutable of mutable arguments is not defensible. My motivation for doing so is to streamline passing variables to Python Parsl app.

Perhaps a better description of the problem I'm having is that Parsl throws errors if the default arguments for an App do not support comparison operators. When encountering this error, I wasn't aware that my arguments need have supported equality and am not sure if there is any way to get around this.
Does this work with:
```
parsl/app/app.py 
> ---> 49             if sig.parameters[s].default != Parameter.empty:
```

swapped to: (abusing the non-commutativity of python equality):

```
         if Parameter.empty != sig.parameters[s].default:
```
Thanks @benclifford! That doesn't work for me.

I am not immediately seeing why we need to save the kwargs separately at all. I'll submit a PR and @yadudoc can let us know, I might be missing something obvious here.

Resolving patch:
diff --git a/parsl/app/app.py b/parsl/app/app.py
--- a/parsl/app/app.py
+++ b/parsl/app/app.py
@@ -1,16 +1,14 @@
-"""Definitions for the @App decorator and the APP classes.
-
-The APP class encapsulates a generic leaf task that can be executed asynchronously.
+"""Definitions for the @App decorator and the App classes.
 
+The App class encapsulates a generic leaf task that can be executed asynchronously.
 """
 import logging
-from inspect import signature, Parameter
+from inspect import signature
 
-# Logging moved here in the PEP8 conformance fixes.
 logger = logging.getLogger(__name__)
 
 
-class AppBase (object):
+class AppBase(object):
     """This is the base class that defines the two external facing functions that an App must define.
 
     The  __init__ () which is called when the interpreter sees the definition of the decorated
@@ -43,16 +41,15 @@ def __init__(self, func, executor, walltime=60, sites='all', cache=False, exec_t
         self.sites = sites
         self.cache = cache
 
-        sig = signature(func)
+        params = signature(func).parameters
+
         self.kwargs = {}
-        for s in sig.parameters:
-            if sig.parameters[s].default != Parameter.empty:
-                self.kwargs[s] = sig.parameters[s].default
-
-        self.stdout = sig.parameters['stdout'].default if 'stdout' in sig.parameters else None
-        self.stderr = sig.parameters['stderr'].default if 'stderr' in sig.parameters else None
-        self.inputs = sig.parameters['inputs'].default if 'inputs' in sig.parameters else []
-        self.outputs = sig.parameters['outputs'].default if 'outputs' in sig.parameters else []
+        if 'stdout' in params:
+            self.kwargs['stdout'] = params['stdout'].default
+        if 'stderr' in params:
+            self.kwargs['stderr'] = params['stderr'].default
+        self.outputs = params['outputs'].default if 'outputs' in params else []
+        self.inputs = params['inputs'].default if 'inputs' in params else []
 
     def __call__(self, *args, **kwargs):
         """The __call__ function must be implemented in the subclasses."""
diff --git a/parsl/app/bash_app.py b/parsl/app/bash_app.py
--- a/parsl/app/bash_app.py
+++ b/parsl/app/bash_app.py
@@ -47,11 +47,11 @@ def remote_side_bash_executor(func, *args, **kwargs):
         raise e
 
     # Updating stdout, stderr if values passed at call time.
-    stdout = kwargs.get('stdout', None)
-    stderr = kwargs.get('stderr', None)
-    timeout = kwargs.get('walltime', None)
-    logging.debug("Stdout  : %s", stdout)
-    logging.debug("Stderr  : %s", stderr)
+    stdout = kwargs.get('stdout')
+    stderr = kwargs.get('stderr')
+    timeout = kwargs.get('walltime')
+    logging.debug("Stdout: %s", stdout)
+    logging.debug("Stderr: %s", stderr)
 
     try:
         std_out = open(stdout, 'w') if stdout else None


Test patch:
diff --git a/parsl/tests/test_regression/test_226.py b/parsl/tests/test_regression/test_226.py
new file mode 100644
--- /dev/null
+++ b/parsl/tests/test_regression/test_226.py
@@ -0,0 +1,59 @@
+'''
+Regression test for #226.
+'''
+from parsl import *
+
+from parsl.configs.local import localThreads as config
+dfk = DataFlowKernel(config=config)
+import pandas as pd
+
+
+class Foo(object):
+    def __init__(self, x):
+        self.x = x
+
+    def __eq__(self, value):
+        raise NotImplementedError
+
+
+bar = Foo(1)
+
+
+@App('python', dfk)
+def get_foo_x(a, b=bar, c=None):
+    return b.x
+
+
+data = pd.DataFrame({'x': [None, 2, [3]]})
+
+
+@App('python', dfk)
+def get_dataframe(d=data):
+    return d
+
+
+@App('bash', dfk)
+def echo(msg, postfix='there', stdout='std.out'):
+    return 'echo {} {}'.format(msg, postfix)
+
+
+def test_no_eq():
+    res = get_foo_x('foo').result()
+    assert res == 1, 'Expected 1, returned {}'.format(res)
+
+
+def test_get_dataframe():
+    res = get_dataframe().result()
+    assert res.equals(data), 'Unexpected dataframe'
+
+
+def test_bash_default_arg():
+    echo('hello').result()
+    with open('std.out', 'r') as f:
+        assert f.read().strip() == 'hello there', "Output should be 'hello there'"
+
+
+if __name__ == '__main__':
+    test_no_eq()
+    test_bash_default_arg()
+    test_get_dataframe()