Repository: dask/dask

Issue:
delayed class method
There seems to be an issue using  delayed as decorator in classes. I found a similar issue in the toolz library here that may be related (https://github.com/pytoolz/toolz/issues/219). Example code:

```python
class A:
    @dask.delayed
    def test(self, a, b):
        return a + b
    
t = A()
t.test(3, 4).compute()
```

produces the following error:

```python
TypeError: test() missing 1 required positional argument: 'b'
```
Removing the decorator and using:

```python
dask.delayed(t.test)(3, 4).compute()
```
works as expected.

Versions: dask=0.17.1, cytoolz=0.9.0.1, Python=3.6.4


Discussion:
As a workaround, can always rewrite this to use a `delayed` function like so...

```python
class A:
    def test(self, a, b):
        return A_test(self, a, b)

@dask.delayed
def A_test(A_obj, a, b):
    return a + b
    
t = A()
t.test(3, 4).compute()
```
I looked at this and it seems @eddieko is absolutely correct - the linked issue's fix will also work here. I would prefer `types.MethodType` over `curry` since the former is builtin, but other than that all the tests pass and class methods also work.

The cause is somewhat cool - since `@delayed` is a decorator, it basically set `test=delayed(test)`, which means `test` isn't a method but is instead a callable object. However, the wrapper treats `test` as a regular three argument function, which caused the issue. Using descriptor and `__get__` can bind `self` correctly to the first argument when the decorator is on a method, thus resolving the bug.

I'll submit a pull request if all that sounds about right!

Resolving patch:
diff --git a/dask/delayed.py b/dask/delayed.py
--- a/dask/delayed.py
+++ b/dask/delayed.py
@@ -2,6 +2,7 @@
 
 from collections import Iterator
 import operator
+import types
 import uuid
 
 try:
@@ -434,6 +435,11 @@ def __bool__(self):
 
     __nonzero__ = __bool__
 
+    def __get__(self, instance, cls):
+        if instance is None:
+            return self
+        return types.MethodType(self, instance)
+
     @classmethod
     def _get_binary_operator(cls, op, inv=False):
         method = delayed(right(op) if inv else op, pure=True)


Test patch:
diff --git a/dask/tests/test_delayed.py b/dask/tests/test_delayed.py
--- a/dask/tests/test_delayed.py
+++ b/dask/tests/test_delayed.py
@@ -2,6 +2,7 @@
 from operator import add, setitem
 import pickle
 from random import random
+import types
 
 from toolz import identity, partial, merge
 import pytest
@@ -510,3 +511,47 @@ def test_keys_from_array():
     xs = [delayed(inc)(x) for x in X]
 
     _check_dsk(xs[0].dask)
+
+
+# Mostly copied from https://github.com/pytoolz/toolz/pull/220
+def test_delayed_decorator_on_method():
+    class A(object):
+        BASE = 10
+
+        def __init__(self, base):
+            self.BASE = base
+
+        @delayed
+        def addmethod(self, x, y):
+            return self.BASE + x + y
+
+        @classmethod
+        @delayed
+        def addclass(cls, x, y):
+            return cls.BASE + x + y
+
+        @staticmethod
+        @delayed
+        def addstatic(x, y):
+            return x + y
+
+    a = A(100)
+    assert a.addmethod(3, 4).compute() == 107
+    assert A.addmethod(a, 3, 4).compute() == 107
+
+    assert a.addclass(3, 4).compute() == 17
+    assert A.addclass(3, 4).compute() == 17
+
+    assert a.addstatic(3, 4).compute() == 7
+    assert A.addstatic(3, 4).compute() == 7
+
+    # We want the decorated methods to be actual methods for instance methods
+    # and class methods since their first arguments are the object and the
+    # class respectively. Or in other words, the first argument is generated by
+    # the runtime based on the object/class before the dot.
+    assert isinstance(a.addmethod, types.MethodType)
+    assert isinstance(A.addclass, types.MethodType)
+
+    # For static methods (and regular functions), the decorated methods should
+    # be Delayed objects.
+    assert isinstance(A.addstatic, Delayed)