Repository: dask/dask

Issue:
funcname fails on multipledispatch objects
dask.utils.funcname looks for a `funcs` attribute on a callable.  I suspect that this is to ducktype against a `toolz.Compose` object.  It then get the first of those functions by calling `func.funcs[0]`.

Unfortunately `multipledispatch` also has a `.funcs` attribute, but it's a `dict`, so we get a `KeyError`.

http://stackoverflow.com/questions/40873835/dask-visualize-keyerror/40889193


Discussion:

Resolving patch:
diff --git a/dask/utils.py b/dask/utils.py
--- a/dask/utils.py
+++ b/dask/utils.py
@@ -620,17 +620,33 @@ def wrapped(*args, **kwargs):
     return wrapper
 
 
-def funcname(func, full=False):
+def funcname(func):
     """Get the name of a function."""
-    while hasattr(func, 'func'):
-        func = func.func
+    # functools.partial
+    if isinstance(func, functools.partial):
+        return funcname(func.func)
+    # methodcaller
+    if isinstance(func, methodcaller):
+        return func.method
+
+    module_name = getattr(func, '__module__', None) or ''
+    type_name = getattr(type(func), '__name__', None) or ''
+
+    # toolz.curry
+    if 'toolz' in module_name and 'curry' == type_name:
+        return func.func_name
+    # multipledispatch objects
+    if 'multipledispatch' in module_name and 'Dispatcher' == type_name:
+        return func.name
+
+    # All other callables
     try:
-        if full:
-            return func.__qualname__.strip('<>')
-        else:
-            return func.__name__.strip('<>')
+        name = func.__name__
+        if name == '<lambda>':
+            return 'lambda'
+        return name
     except:
-        return str(func).strip('<>')
+        return str(func)
 
 
 def ensure_bytes(s):


Test patch:
diff --git a/dask/tests/test_utils.py b/dask/tests/test_utils.py
--- a/dask/tests/test_utils.py
+++ b/dask/tests/test_utils.py
@@ -1,5 +1,6 @@
 import os
 import pickle
+import functools
 
 import numpy as np
 import pytest
@@ -8,7 +9,8 @@
 from dask.utils import (textblock, filetext, takes_multiple_arguments,
                         Dispatch, tmpfile, random_state_data, file_size,
                         infer_storage_options, eq_strict, memory_repr,
-                        methodcaller, M, skip_doctest, SerializableLock)
+                        methodcaller, M, skip_doctest, SerializableLock,
+                        funcname)
 
 
 SKIP_XZ = pytest.mark.skipif(not LZMA_AVAILABLE, reason="no lzma library")
@@ -263,3 +265,41 @@ def test_SerializableLock_name_collision():
     assert a.lock is not b.lock
     assert a.lock is c.lock
     assert d.lock not in (a.lock, b.lock, c.lock)
+
+
+def test_funcname():
+    def foo(a, b, c):
+        pass
+
+    assert funcname(foo) == 'foo'
+    assert funcname(functools.partial(foo, a=1)) == 'foo'
+    assert funcname(M.sum) == 'sum'
+    assert funcname(lambda: 1) == 'lambda'
+
+    class Foo(object):
+        pass
+
+    assert funcname(Foo) == 'Foo'
+    assert 'Foo' in funcname(Foo())
+
+
+def test_funcname_toolz():
+    toolz = pytest.importorskip('toolz')
+
+    @toolz.curry
+    def foo(a, b, c):
+        pass
+
+    assert funcname(foo) == 'foo'
+    assert funcname(foo(1)) == 'foo'
+
+
+def test_funcname_multipledispatch():
+    md = pytest.importorskip('multipledispatch')
+
+    @md.dispatch(int, int, int)
+    def foo(a, b, c):
+        pass
+
+    assert funcname(foo) == 'foo'
+    assert funcname(functools.partial(foo, a=1)) == 'foo'