Repository: dask/dask

Issue:
isin issue with numpy 1.12 on Python 2
I received the following report on `da.isin` through a side channel.   cc @jakirkham 

We're using numpy 1.12, which doesn't have numpy.isin().  dask/dask#3363 leads to:

```
% python -c 'import dask.array'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "dask/array/__init__.py", line 8, in <module>
    from .routines import (take, choose, argwhere, where, coarsen, insert,
  File "dask/array/routines.py", line 790, in <module>
    def isin(element, test_elements, assume_unique=False, invert=False):
  File "/opt/python/python-2.7/lib64/python2.7/functools.py", line 33, in update_wrapper
    setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'NoneType' object has no attribute '__module__'
```
where `wrapped` is `None` and `attr` is `'__module__'`.

I think this only happens with Python 2's functools.

py3: https://github.com/python/cpython/blame/0f914b5b5f6ba186afd7112fc851c97247076f70/Lib/functools.py#L52
py2: https://github.com/python/cpython/blame/77f0a41d72886513f5e9277e5587455745c29ac1/Lib/functools.py#L32



Discussion:
Interesting, thanks for the info. Also cc @shoyer.
This reminds of a more general discussion about how we document API ( https://github.com/dask/dask/issues/3265 ) that we should have at some point. ;)

As to the particular problem, we have a few options. None particularly difficult (some are easier though).

1. Only define Dask Array `isin` for versions of NumPy that have it.
2. Vendor a copy of NumPy's `isin` implementation and use it.
3. ~~Implement `isin` with `in1d` + `reshape` and~~ copy/rewrite the docstring from NumPy.
4. Vendor/implement our own `wraps` (combined with part of 3).
5. Something else?

Thoughts?
Oops. I thought all we would miss here is the docstring on old versions of NumPy, but it looks like that causes a crash at import time :(.

`@wraps(getattr(np, 'isin', np.1nd))` would probably be an adequate work-around for now.

I didn't bother to vender a copy of numpy's `isin` because it was just as easy to write dask's `isin` in terms of `np.in1d`.
Yeah, I thought the Python 3, NumPy 1.12 and NumPy 1.10 CI combinations were sufficient to rule out this sort of issue, but guess not. Easily fixable though. So wouldn't stress about it.

That's probably ok. It's a little confusing based on the return type, but expect users can figure this out. Also it is no worse than many of the other docstrings using this same strategy.

Ultimately we should discuss how to fix the docstrings to be clearer at some point, but the discussion need not happen now in this issue. Just should happen eventually with this just highlighting another pain point.

That's right. Forgot we were already using `in1d`. Edited the list in my original comment accordingly.

Resolving patch:
diff --git a/dask/array/routines.py b/dask/array/routines.py
--- a/dask/array/routines.py
+++ b/dask/array/routines.py
@@ -16,6 +16,7 @@
 from ..base import tokenize
 from . import numpy_compat, chunk
 from .creation import arange
+from .utils import safe_wraps
 from .wrap import ones
 
 from .core import (Array, map_blocks, elemwise, from_array, asarray,
@@ -786,7 +787,7 @@ def _isin_kernel(element, test_elements, assume_unique=False):
     return values.reshape(element.shape + (1,) * test_elements.ndim)
 
 
-@wraps(getattr(np, 'isin', None))
+@safe_wraps(getattr(np, 'isin', None))
 def isin(element, test_elements, assume_unique=False, invert=False):
     element = asarray(element)
     test_elements = asarray(test_elements)
diff --git a/dask/array/utils.py b/dask/array/utils.py
--- a/dask/array/utils.py
+++ b/dask/array/utils.py
@@ -2,6 +2,7 @@
 
 from distutils.version import LooseVersion
 import difflib
+import functools
 import math
 import os
 
@@ -129,3 +130,14 @@ def assert_eq(a, b, check_shape=True, **kwargs):
         assert c
 
     return True
+
+
+def safe_wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS):
+    """Like functools.wraps, but safe to use even if wrapped is not a function.
+
+    Only needed on Python 2.
+    """
+    if all(hasattr(wrapped, attr) for attr in assigned):
+        return functools.wraps(wrapped, assigned=assigned)
+    else:
+        return lambda x: x


Test patch: