Repository: dask/distributed

Issue:
Add helpful error message when passing a bad argument to Client
Today I made a typo which resulted in an unhelpful error message. I created a `Client` from a `LocalCluster` but forgot to actually instantiate the `LocalCluster`. I assume I'm not the first and won't be the last.

```python
from dask.distributed import LocalCluster, Client
cluster = LocalCluster  # Note the missing ()
client = Client(cluster)
```

This results in a `TypeError` complaining about expecting a string but get a property.

```python-traceback
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-3-82a29784114b> in <module>
      1 cluster = LocalCluster
----> 2 client = Client(cluster)

~/Projects/dask/distributed/distributed/client.py in __init__(self, address, loop, timeout, set_as_default, scheduler_file, security, asynchronous, name, heartbeat_interval, serializers, deserializers, extensions, direct_to_workers, connection_limit, **kwargs)
    734             ext(self)
    735 
--> 736         self.start(timeout=timeout)
    737         Client._instances.add(self)
    738 

~/Projects/dask/distributed/distributed/client.py in start(self, **kwargs)
    938             self._started = asyncio.ensure_future(self._start(**kwargs))
    939         else:
--> 940             sync(self.loop, self._start, **kwargs)
    941 
    942     def __await__(self):

~/Projects/dask/distributed/distributed/utils.py in sync(loop, func, callback_timeout, *args, **kwargs)
    337     if error[0]:
    338         typ, exc, tb = error[0]
--> 339         raise exc.with_traceback(tb)
    340     else:
    341         return result[0]

~/Projects/dask/distributed/distributed/utils.py in f()
    321             if callback_timeout is not None:
    322                 future = asyncio.wait_for(future, callback_timeout)
--> 323             result[0] = yield future
    324         except Exception as exc:
    325             error[0] = sys.exc_info()

~/miniconda3/envs/dask/lib/python3.7/site-packages/tornado/gen.py in run(self)
    733 
    734                     try:
--> 735                         value = future.result()
    736                     except Exception:
    737                         exc_info = sys.exc_info()

~/Projects/dask/distributed/distributed/client.py in _start(self, timeout, **kwargs)
   1031 
   1032         if self.scheduler is None:
-> 1033             self.scheduler = self.rpc(address)
   1034         self.scheduler_comm = None
   1035 

~/Projects/dask/distributed/distributed/core.py in __call__(self, addr, ip, port)
    915     def __call__(self, addr=None, ip=None, port=None):
    916         """ Cached rpc objects """
--> 917         addr = addr_from_args(addr=addr, ip=ip, port=port)
    918         return PooledRPCCall(
    919             addr, self, serializers=self.serializers, deserializers=self.deserializers

~/Projects/dask/distributed/distributed/core.py in addr_from_args(addr, ip, port)
    616     if isinstance(addr, tuple):
    617         addr = unparse_host_port(*addr)
--> 618     return normalize_address(addr)
    619 
    620 

~/Projects/dask/distributed/distributed/comm/addressing.py in normalize_address(addr)
     52     'tcp://[::1]'
     53     """
---> 54     return unparse_address(*parse_address(addr))
     55 
     56 

~/Projects/dask/distributed/distributed/comm/addressing.py in parse_address(addr, strict)
     19     """
     20     if not isinstance(addr, str):
---> 21         raise TypeError("expected str, got %r" % addr.__class__.__name__)
     22     scheme, sep, loc = addr.rpartition("://")
     23     if strict and not sep:

TypeError: expected str, got 'property'
```

It would be nice to catch this and raise a more helpful error about the kind of things that can be passed to `Client`. Perhaps even test if the thing passed in is a `Cluster` class rather than an instance of the class and give an error about instantiating.


Discussion:
Hi - I am looking for a first issue to start contributing. Is the process to just open a PR from my forked repo which references this issue?
Hi @joshreback yes, that is the process.  Fork, make a new branch, commit some code, and then submit a PR from your fork/branch to the master branch of this fork.

Resolving patch:
diff --git a/distributed/client.py b/distributed/client.py
--- a/distributed/client.py
+++ b/distributed/client.py
@@ -650,13 +650,19 @@ def __init__(
 
         if isinstance(address, (rpc, PooledRPCCall)):
             self.scheduler = address
-        elif hasattr(address, "scheduler_address"):
+        elif isinstance(getattr(address, "scheduler_address", None), str):
             # It's a LocalCluster or LocalCluster-compatible object
             self.cluster = address
             with suppress(AttributeError):
                 loop = address.loop
             if security is None:
                 security = getattr(self.cluster, "security", None)
+        elif address is not None and not isinstance(address, str):
+            raise TypeError(
+                "Scheduler address must be a string or a Cluster instance, got {}".format(
+                    type(address)
+                )
+            )
 
         if security is None:
             security = Security()


Test patch:
diff --git a/distributed/tests/test_client.py b/distributed/tests/test_client.py
--- a/distributed/tests/test_client.py
+++ b/distributed/tests/test_client.py
@@ -1847,6 +1847,15 @@ def test_bad_address():
         assert "connect" in str(e).lower()
 
 
+def test_informative_error_on_cluster_type():
+    with pytest.raises(TypeError) as exc_info:
+        Client(LocalCluster)
+
+    assert "Scheduler address must be a string or a Cluster instance" in str(
+        exc_info.value
+    )
+
+
 @gen_cluster(client=True)
 async def test_long_error(c, s, a, b):
     def bad(x):