We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Version mismatch warning is a little scary
## Background

When clients/scheduler/workers have mismatched versions, users get an informative error message like the following:

```
/home/mrocklin/workspace/distributed/distributed/client.py:1079: VersionMismatchWarning: Mismatched versions found

blosc
+---------------------------+---------+
|                           | version |
+---------------------------+---------+
| client                    | None    |
| scheduler                 | 1.9.1   |
| tcp://172.31.15.170:46853 | 1.9.1   |
| tcp://172.31.18.92:41153  | 1.9.1   |
| tcp://172.31.42.33:42009  | 1.9.1   |
| tcp://172.31.7.159:38461  | 1.9.1   |
+---------------------------+---------+

cloudpickle
+---------------------------+---------+
|                           | version |
+---------------------------+---------+
| client                    | 1.3.0   |
| scheduler                 | 1.4.0   |
| tcp://172.31.15.170:46853 | 1.4.0   |
| tcp://172.31.18.92:41153  | 1.4.0   |
| tcp://172.31.42.33:42009  | 1.4.0   |
| tcp://172.31.7.159:38461  | 1.4.0   |
+---------------------------+---------+

dask
+---------------------------+---------------------+
|                           | version             |
+---------------------------+---------------------+
| client                    | 2.14.0+34.g8ab7f942 |
| scheduler                 | 2.15.0              |
| tcp://172.31.15.170:46853 | 2.15.0              |
| tcp://172.31.18.92:41153  | 2.15.0              |
| tcp://172.31.42.33:42009  | 2.15.0              |
| tcp://172.31.7.159:38461  | 2.15.0              |
+---------------------------+---------------------+

distributed
+---------------------------+---------------------+
|                           | version             |
+---------------------------+---------------------+
| client                    | 2.14.0+47.gb4dc9c64 |
| scheduler                 | 2.15.0              |
| tcp://172.31.15.170:46853 | 2.15.0              |
| tcp://172.31.18.92:41153  | 2.15.0              |
| tcp://172.31.42.33:42009  | 2.15.0              |
| tcp://172.31.7.159:38461  | 2.15.0              |
+---------------------------+---------------------+

lz4
+---------------------------+---------+
|                           | version |
+---------------------------+---------+
| client                    | 2.2.1   |
| scheduler                 | 3.0.2   |
| tcp://172.31.15.170:46853 | 3.0.2   |
| tcp://172.31.18.92:41153  | 3.0.2   |
| tcp://172.31.42.33:42009  | 3.0.2   |
| tcp://172.31.7.159:38461  | 3.0.2   |
+---------------------------+---------+

msgpack
+---------------------------+---------+
|                           | version |
+---------------------------+---------+
| client                    | 0.6.2   |
| scheduler                 | 1.0.0   |
| tcp://172.31.15.170:46853 | 1.0.0   |
| tcp://172.31.18.92:41153  | 1.0.0   |
| tcp://172.31.42.33:42009  | 1.0.0   |
| tcp://172.31.7.159:38461  | 1.0.0   |
+---------------------------+---------+

python
+---------------------------+---------------+
|                           | version       |
+---------------------------+---------------+
| client                    | 3.7.6.final.0 |
| scheduler                 | 3.7.4.final.0 |
| tcp://172.31.15.170:46853 | 3.7.4.final.0 |
| tcp://172.31.18.92:41153  | 3.7.4.final.0 |
| tcp://172.31.42.33:42009  | 3.7.4.final.0 |
| tcp://172.31.7.159:38461  | 3.7.4.final.0 |
+---------------------------+---------------+
  warnings.warn(version_module.VersionMismatchWarning(msg[0]["warning"]))
```

This is generally pretty great.  We used to get a ton of github issues that reduced down to version mismatches, and now we don't.  Hooray for informative error messages.

## Moving forward

However, I've run into a couple of issues that arise with these messages in practice, where I think that we might be able to improve them a bit.

1.  They can get very long, especially if you have lots of workers.
2.  We don't call out really important issues in relation to less important issues.  It's entirely ok if your `msgpack` version is a little off, but probably not ok if some machines have `lz4` and some don't.  

So I wonder if we might reorganize this message a bit.  We might have something like the following:

```
+-----------+----------+------------+------------+
| Package   |  client  |  scheduler |    workers |
+-----------+----------+------------+------------+
| python    |    3.7   |    3.8     | {3.7, 3.8} |
| lz4       |   ...    |    ...     |     ...    |
| msgpack   |          |    ...     |     ...    |
+-----------+----------+------------+------------+

Notes:

-  msgpack: Variation is ok, as long as everything is above 0.6
-  lz4: Variation is ok, but missing libraries are not
-  python: Variation is sometimes ok, sometimes not.  It depends on your workloads
```

We pack down the one-line-per-worker policy, and instead include sets of versions in the table if necessary.  This makes it a bit harder to debug if one of the workers is mismatched, but I think that this behavior isn't common.  

We include optional prose around each of the libraries and include that prose if that library is found to be mismatched.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `distributed/versions.py`
Content:
```
1 """ utilities for package version introspection """
2 
3 from __future__ import print_function, division, absolute_import
4 
5 from collections import defaultdict
6 import platform
7 import struct
8 import os
9 import sys
10 import importlib
11 
12 
13 required_packages = [
14     ("dask", lambda p: p.__version__),
15     ("distributed", lambda p: p.__version__),
16     ("msgpack", lambda p: ".".join([str(v) for v in p.version])),
17     ("cloudpickle", lambda p: p.__version__),
18     ("tornado", lambda p: p.version),
19     ("toolz", lambda p: p.__version__),
20 ]
21 
22 optional_packages = [
23     ("numpy", lambda p: p.__version__),
24     ("lz4", lambda p: p.__version__),
25     ("blosc", lambda p: p.__version__),
26 ]
27 
28 
29 # only these scheduler packages will be checked for version mismatch
30 scheduler_relevant_packages = set(pkg for pkg, _ in required_packages) | set(
31     ["lz4", "blosc"]
32 )
33 
34 
35 def get_versions(packages=None):
36     """
37     Return basic information on our software installation, and our installed versions of packages.
38     """
39     if packages is None:
40         packages = []
41 
42     d = {
43         "host": get_system_info(),
44         "packages": get_package_info(
45             required_packages + optional_packages + list(packages)
46         ),
47     }
48 
49     return d
50 
51 
52 def get_system_info():
53     (sysname, nodename, release, version, machine, processor) = platform.uname()
54     host = {
55         "python": "%d.%d.%d.%s.%s" % sys.version_info[:],
56         "python-bits": struct.calcsize("P") * 8,
57         "OS": "%s" % sysname,
58         "OS-release": "%s" % release,
59         "machine": "%s" % machine,
60         "processor": "%s" % processor,
61         "byteorder": "%s" % sys.byteorder,
62         "LC_ALL": "%s" % os.environ.get("LC_ALL", "None"),
63         "LANG": "%s" % os.environ.get("LANG", "None"),
64     }
65 
66     return host
67 
68 
69 def version_of_package(pkg):
70     """ Try a variety of common ways to get the version of a package """
71     from .utils import ignoring
72 
73     with ignoring(AttributeError):
74         return pkg.__version__
75     with ignoring(AttributeError):
76         return str(pkg.version)
77     with ignoring(AttributeError):
78         return ".".join(map(str, pkg.version_info))
79     return None
80 
81 
82 def get_package_info(pkgs):
83     """ get package versions for the passed required & optional packages """
84 
85     pversions = [("python", ".".join(map(str, sys.version_info)))]
86     for pkg in pkgs:
87         if isinstance(pkg, (tuple, list)):
88             modname, ver_f = pkg
89         else:
90             modname = pkg
91             ver_f = version_of_package
92 
93         if ver_f is None:
94             ver_f = version_of_package
95 
96         try:
97             mod = importlib.import_module(modname)
98             ver = ver_f(mod)
99             pversions.append((modname, ver))
100         except Exception:
101             pversions.append((modname, None))
102 
103     return dict(pversions)
104 
105 
106 def error_message(scheduler, workers, client, client_name="client"):
107     from .utils import asciitable
108 
109     nodes = {**{client_name: client}, **{"scheduler": scheduler}, **workers}
110 
111     # Hold all versions, e.g. versions["scheduler"]["distributed"] = 2.9.3
112     node_packages = defaultdict(dict)
113 
114     # Collect all package versions
115     packages = set()
116     for node, info in nodes.items():
117         if info is None or not (isinstance(info, dict)) or "packages" not in info:
118             node_packages[node] = defaultdict(lambda: "UNKNOWN")
119         else:
120             node_packages[node] = defaultdict(lambda: "MISSING")
121             for pkg, version in info["packages"].items():
122                 node_packages[node][pkg] = version
123                 packages.add(pkg)
124 
125     errs = []
126     for pkg in sorted(packages):
127         versions = set(
128             node_packages[node][pkg]
129             for node in nodes
130             if node != "scheduler" or pkg in scheduler_relevant_packages
131         )
132         if len(versions) <= 1:
133             continue
134         rows = [
135             (node_name, node_packages[node_name][pkg]) for node_name in nodes.keys()
136         ]
137         errs.append("%s\n%s" % (pkg, asciitable(["", "version"], rows)))
138     if errs:
139         return "Mismatched versions found\n" "\n" "%s" % ("\n\n".join(errs))
140     else:
141         return ""
142 
143 
144 class VersionMismatchWarning(Warning):
145     """Indicates version mismatch between nodes"""
146
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/distributed/versions.py b/distributed/versions.py
--- a/distributed/versions.py
+++ b/distributed/versions.py
@@ -2,7 +2,6 @@
 
 from __future__ import print_function, division, absolute_import
 
-from collections import defaultdict
 import platform
 import struct
 import os
@@ -32,6 +31,14 @@
 )
 
 
+# notes to be displayed for mismatch packages
+notes_mismatch_package = {
+    "msgpack": "Variation is ok, as long as everything is above 0.6",
+    "lz4": "Variation is ok, but missing libraries are not",
+    "python": "Variation is sometimes ok, sometimes not. It depends on your workloads",
+}
+
+
 def get_versions(packages=None):
     """
     Return basic information on our software installation, and our installed versions of packages.
@@ -106,37 +113,56 @@
 def error_message(scheduler, workers, client, client_name="client"):
     from .utils import asciitable
 
-    nodes = {**{client_name: client}, **{"scheduler": scheduler}, **workers}
-
-    # Hold all versions, e.g. versions["scheduler"]["distributed"] = 2.9.3
-    node_packages = defaultdict(dict)
+    client = client.get("packages") if client else "UNKNOWN"
+    scheduler = scheduler.get("packages") if scheduler else "UNKNOWN"
+    workers = {k: v.get("packages") if v else "UNKNOWN" for k, v in workers.items()}
 
-    # Collect all package versions
     packages = set()
-    for node, info in nodes.items():
-        if info is None or not (isinstance(info, dict)) or "packages" not in info:
-            node_packages[node] = defaultdict(lambda: "UNKNOWN")
-        else:
-            node_packages[node] = defaultdict(lambda: "MISSING")
-            for pkg, version in info["packages"].items():
-                node_packages[node][pkg] = version
-                packages.add(pkg)
+    packages.update(client)
+    packages.update(scheduler)
+    for worker in workers:
+        packages.update(workers.get(worker))
 
     errs = []
+    notes = []
     for pkg in sorted(packages):
-        versions = set(
-            node_packages[node][pkg]
-            for node in nodes
-            if node != "scheduler" or pkg in scheduler_relevant_packages
+        versions = set()
+        scheduler_version = (
+            scheduler.get(pkg, "MISSING") if isinstance(scheduler, dict) else scheduler
+        )
+        if pkg in scheduler_relevant_packages:
+            versions.add(scheduler_version)
+
+        client_version = (
+            client.get(pkg, "MISSING") if isinstance(client, dict) else client
         )
+        versions.add(client_version)
+
+        worker_versions = set(
+            workers[w].get(pkg, "MISSING")
+            if isinstance(workers[w], dict)
+            else workers[w]
+            for w in workers
+        )
+        versions |= worker_versions
+
         if len(versions) <= 1:
             continue
-        rows = [
-            (node_name, node_packages[node_name][pkg]) for node_name in nodes.keys()
-        ]
-        errs.append("%s\n%s" % (pkg, asciitable(["", "version"], rows)))
+        if len(worker_versions) == 1:
+            worker_versions = list(worker_versions)[0]
+        elif len(worker_versions) == 0:
+            worker_versions = None
+
+        errs.append((pkg, client_version, scheduler_version, worker_versions))
+        if pkg in notes_mismatch_package.keys():
+            notes.append(f"-  {pkg}: {notes_mismatch_package[pkg]}")
+
     if errs:
-        return "Mismatched versions found\n" "\n" "%s" % ("\n\n".join(errs))
+        err_table = asciitable(["Package", client_name, "scheduler", "workers"], errs)
+        err_msg = f"Mismatched versions found\n\n{err_table}"
+        if notes:
+            err_msg += "\nNotes: \n{}".format("\n".join(notes))
+        return err_msg
     else:
         return ""

{"golden_diff": "diff --git a/distributed/versions.py b/distributed/versions.py\n--- a/distributed/versions.py\n+++ b/distributed/versions.py\n@@ -2,7 +2,6 @@\n \n from __future__ import print_function, division, absolute_import\n \n-from collections import defaultdict\n import platform\n import struct\n import os\n@@ -32,6 +31,14 @@\n )\n \n \n+# notes to be displayed for mismatch packages\n+notes_mismatch_package = {\n+    \"msgpack\": \"Variation is ok, as long as everything is above 0.6\",\n+    \"lz4\": \"Variation is ok, but missing libraries are not\",\n+    \"python\": \"Variation is sometimes ok, sometimes not. It depends on your workloads\",\n+}\n+\n+\n def get_versions(packages=None):\n     \"\"\"\n     Return basic information on our software installation, and our installed versions of packages.\n@@ -106,37 +113,56 @@\n def error_message(scheduler, workers, client, client_name=\"client\"):\n     from .utils import asciitable\n \n-    nodes = {**{client_name: client}, **{\"scheduler\": scheduler}, **workers}\n-\n-    # Hold all versions, e.g. versions[\"scheduler\"][\"distributed\"] = 2.9.3\n-    node_packages = defaultdict(dict)\n+    client = client.get(\"packages\") if client else \"UNKNOWN\"\n+    scheduler = scheduler.get(\"packages\") if scheduler else \"UNKNOWN\"\n+    workers = {k: v.get(\"packages\") if v else \"UNKNOWN\" for k, v in workers.items()}\n \n-    # Collect all package versions\n     packages = set()\n-    for node, info in nodes.items():\n-        if info is None or not (isinstance(info, dict)) or \"packages\" not in info:\n-            node_packages[node] = defaultdict(lambda: \"UNKNOWN\")\n-        else:\n-            node_packages[node] = defaultdict(lambda: \"MISSING\")\n-            for pkg, version in info[\"packages\"].items():\n-                node_packages[node][pkg] = version\n-                packages.add(pkg)\n+    packages.update(client)\n+    packages.update(scheduler)\n+    for worker in workers:\n+        packages.update(workers.get(worker))\n \n     errs = []\n+    notes = []\n     for pkg in sorted(packages):\n-        versions = set(\n-            node_packages[node][pkg]\n-            for node in nodes\n-            if node != \"scheduler\" or pkg in scheduler_relevant_packages\n+        versions = set()\n+        scheduler_version = (\n+            scheduler.get(pkg, \"MISSING\") if isinstance(scheduler, dict) else scheduler\n+        )\n+        if pkg in scheduler_relevant_packages:\n+            versions.add(scheduler_version)\n+\n+        client_version = (\n+            client.get(pkg, \"MISSING\") if isinstance(client, dict) else client\n         )\n+        versions.add(client_version)\n+\n+        worker_versions = set(\n+            workers[w].get(pkg, \"MISSING\")\n+            if isinstance(workers[w], dict)\n+            else workers[w]\n+            for w in workers\n+        )\n+        versions |= worker_versions\n+\n         if len(versions) <= 1:\n             continue\n-        rows = [\n-            (node_name, node_packages[node_name][pkg]) for node_name in nodes.keys()\n-        ]\n-        errs.append(\"%s\\n%s\" % (pkg, asciitable([\"\", \"version\"], rows)))\n+        if len(worker_versions) == 1:\n+            worker_versions = list(worker_versions)[0]\n+        elif len(worker_versions) == 0:\n+            worker_versions = None\n+\n+        errs.append((pkg, client_version, scheduler_version, worker_versions))\n+        if pkg in notes_mismatch_package.keys():\n+            notes.append(f\"-  {pkg}: {notes_mismatch_package[pkg]}\")\n+\n     if errs:\n-        return \"Mismatched versions found\\n\" \"\\n\" \"%s\" % (\"\\n\\n\".join(errs))\n+        err_table = asciitable([\"Package\", client_name, \"scheduler\", \"workers\"], errs)\n+        err_msg = f\"Mismatched versions found\\n\\n{err_table}\"\n+        if notes:\n+            err_msg += \"\\nNotes: \\n{}\".format(\"\\n\".join(notes))\n+        return err_msg\n     else:\n         return \"\"\n", "issue": "Version mismatch warning is a little scary\n## Background\r\n\r\nWhen clients/scheduler/workers have mismatched versions, users get an informative error message like the following:\r\n\r\n```\r\n/home/mrocklin/workspace/distributed/distributed/client.py:1079: VersionMismatchWarning: Mismatched versions found\r\n\r\nblosc\r\n+---------------------------+---------+\r\n|                           | version |\r\n+---------------------------+---------+\r\n| client                    | None    |\r\n| scheduler                 | 1.9.1   |\r\n| tcp://172.31.15.170:46853 | 1.9.1   |\r\n| tcp://172.31.18.92:41153  | 1.9.1   |\r\n| tcp://172.31.42.33:42009  | 1.9.1   |\r\n| tcp://172.31.7.159:38461  | 1.9.1   |\r\n+---------------------------+---------+\r\n\r\ncloudpickle\r\n+---------------------------+---------+\r\n|                           | version |\r\n+---------------------------+---------+\r\n| client                    | 1.3.0   |\r\n| scheduler                 | 1.4.0   |\r\n| tcp://172.31.15.170:46853 | 1.4.0   |\r\n| tcp://172.31.18.92:41153  | 1.4.0   |\r\n| tcp://172.31.42.33:42009  | 1.4.0   |\r\n| tcp://172.31.7.159:38461  | 1.4.0   |\r\n+---------------------------+---------+\r\n\r\ndask\r\n+---------------------------+---------------------+\r\n|                           | version             |\r\n+---------------------------+---------------------+\r\n| client                    | 2.14.0+34.g8ab7f942 |\r\n| scheduler                 | 2.15.0              |\r\n| tcp://172.31.15.170:46853 | 2.15.0              |\r\n| tcp://172.31.18.92:41153  | 2.15.0              |\r\n| tcp://172.31.42.33:42009  | 2.15.0              |\r\n| tcp://172.31.7.159:38461  | 2.15.0              |\r\n+---------------------------+---------------------+\r\n\r\ndistributed\r\n+---------------------------+---------------------+\r\n|                           | version             |\r\n+---------------------------+---------------------+\r\n| client                    | 2.14.0+47.gb4dc9c64 |\r\n| scheduler                 | 2.15.0              |\r\n| tcp://172.31.15.170:46853 | 2.15.0              |\r\n| tcp://172.31.18.92:41153  | 2.15.0              |\r\n| tcp://172.31.42.33:42009  | 2.15.0              |\r\n| tcp://172.31.7.159:38461  | 2.15.0              |\r\n+---------------------------+---------------------+\r\n\r\nlz4\r\n+---------------------------+---------+\r\n|                           | version |\r\n+---------------------------+---------+\r\n| client                    | 2.2.1   |\r\n| scheduler                 | 3.0.2   |\r\n| tcp://172.31.15.170:46853 | 3.0.2   |\r\n| tcp://172.31.18.92:41153  | 3.0.2   |\r\n| tcp://172.31.42.33:42009  | 3.0.2   |\r\n| tcp://172.31.7.159:38461  | 3.0.2   |\r\n+---------------------------+---------+\r\n\r\nmsgpack\r\n+---------------------------+---------+\r\n|                           | version |\r\n+---------------------------+---------+\r\n| client                    | 0.6.2   |\r\n| scheduler                 | 1.0.0   |\r\n| tcp://172.31.15.170:46853 | 1.0.0   |\r\n| tcp://172.31.18.92:41153  | 1.0.0   |\r\n| tcp://172.31.42.33:42009  | 1.0.0   |\r\n| tcp://172.31.7.159:38461  | 1.0.0   |\r\n+---------------------------+---------+\r\n\r\npython\r\n+---------------------------+---------------+\r\n|                           | version       |\r\n+---------------------------+---------------+\r\n| client                    | 3.7.6.final.0 |\r\n| scheduler                 | 3.7.4.final.0 |\r\n| tcp://172.31.15.170:46853 | 3.7.4.final.0 |\r\n| tcp://172.31.18.92:41153  | 3.7.4.final.0 |\r\n| tcp://172.31.42.33:42009  | 3.7.4.final.0 |\r\n| tcp://172.31.7.159:38461  | 3.7.4.final.0 |\r\n+---------------------------+---------------+\r\n  warnings.warn(version_module.VersionMismatchWarning(msg[0][\"warning\"]))\r\n```\r\n\r\nThis is generally pretty great.  We used to get a ton of github issues that reduced down to version mismatches, and now we don't.  Hooray for informative error messages.\r\n\r\n## Moving forward\r\n\r\nHowever, I've run into a couple of issues that arise with these messages in practice, where I think that we might be able to improve them a bit.\r\n\r\n1.  They can get very long, especially if you have lots of workers.\r\n2.  We don't call out really important issues in relation to less important issues.  It's entirely ok if your `msgpack` version is a little off, but probably not ok if some machines have `lz4` and some don't.  \r\n\r\nSo I wonder if we might reorganize this message a bit.  We might have something like the following:\r\n\r\n```\r\n+-----------+----------+------------+------------+\r\n| Package   |  client  |  scheduler |    workers |\r\n+-----------+----------+------------+------------+\r\n| python    |    3.7   |    3.8     | {3.7, 3.8} |\r\n| lz4       |   ...    |    ...     |     ...    |\r\n| msgpack   |          |    ...     |     ...    |\r\n+-----------+----------+------------+------------+\r\n\r\nNotes:\r\n\r\n-  msgpack: Variation is ok, as long as everything is above 0.6\r\n-  lz4: Variation is ok, but missing libraries are not\r\n-  python: Variation is sometimes ok, sometimes not.  It depends on your workloads\r\n```\r\n\r\nWe pack down the one-line-per-worker policy, and instead include sets of versions in the table if necessary.  This makes it a bit harder to debug if one of the workers is mismatched, but I think that this behavior isn't common.  \r\n\r\nWe include optional prose around each of the libraries and include that prose if that library is found to be mismatched.\n", "before_files": [{"content": "\"\"\" utilities for package version introspection \"\"\"\n\nfrom __future__ import print_function, division, absolute_import\n\nfrom collections import defaultdict\nimport platform\nimport struct\nimport os\nimport sys\nimport importlib\n\n\nrequired_packages = [\n    (\"dask\", lambda p: p.__version__),\n    (\"distributed\", lambda p: p.__version__),\n    (\"msgpack\", lambda p: \".\".join([str(v) for v in p.version])),\n    (\"cloudpickle\", lambda p: p.__version__),\n    (\"tornado\", lambda p: p.version),\n    (\"toolz\", lambda p: p.__version__),\n]\n\noptional_packages = [\n    (\"numpy\", lambda p: p.__version__),\n    (\"lz4\", lambda p: p.__version__),\n    (\"blosc\", lambda p: p.__version__),\n]\n\n\n# only these scheduler packages will be checked for version mismatch\nscheduler_relevant_packages = set(pkg for pkg, _ in required_packages) | set(\n    [\"lz4\", \"blosc\"]\n)\n\n\ndef get_versions(packages=None):\n    \"\"\"\n    Return basic information on our software installation, and our installed versions of packages.\n    \"\"\"\n    if packages is None:\n        packages = []\n\n    d = {\n        \"host\": get_system_info(),\n        \"packages\": get_package_info(\n            required_packages + optional_packages + list(packages)\n        ),\n    }\n\n    return d\n\n\ndef get_system_info():\n    (sysname, nodename, release, version, machine, processor) = platform.uname()\n    host = {\n        \"python\": \"%d.%d.%d.%s.%s\" % sys.version_info[:],\n        \"python-bits\": struct.calcsize(\"P\") * 8,\n        \"OS\": \"%s\" % sysname,\n        \"OS-release\": \"%s\" % release,\n        \"machine\": \"%s\" % machine,\n        \"processor\": \"%s\" % processor,\n        \"byteorder\": \"%s\" % sys.byteorder,\n        \"LC_ALL\": \"%s\" % os.environ.get(\"LC_ALL\", \"None\"),\n        \"LANG\": \"%s\" % os.environ.get(\"LANG\", \"None\"),\n    }\n\n    return host\n\n\ndef version_of_package(pkg):\n    \"\"\" Try a variety of common ways to get the version of a package \"\"\"\n    from .utils import ignoring\n\n    with ignoring(AttributeError):\n        return pkg.__version__\n    with ignoring(AttributeError):\n        return str(pkg.version)\n    with ignoring(AttributeError):\n        return \".\".join(map(str, pkg.version_info))\n    return None\n\n\ndef get_package_info(pkgs):\n    \"\"\" get package versions for the passed required & optional packages \"\"\"\n\n    pversions = [(\"python\", \".\".join(map(str, sys.version_info)))]\n    for pkg in pkgs:\n        if isinstance(pkg, (tuple, list)):\n            modname, ver_f = pkg\n        else:\n            modname = pkg\n            ver_f = version_of_package\n\n        if ver_f is None:\n            ver_f = version_of_package\n\n        try:\n            mod = importlib.import_module(modname)\n            ver = ver_f(mod)\n            pversions.append((modname, ver))\n        except Exception:\n            pversions.append((modname, None))\n\n    return dict(pversions)\n\n\ndef error_message(scheduler, workers, client, client_name=\"client\"):\n    from .utils import asciitable\n\n    nodes = {**{client_name: client}, **{\"scheduler\": scheduler}, **workers}\n\n    # Hold all versions, e.g. versions[\"scheduler\"][\"distributed\"] = 2.9.3\n    node_packages = defaultdict(dict)\n\n    # Collect all package versions\n    packages = set()\n    for node, info in nodes.items():\n        if info is None or not (isinstance(info, dict)) or \"packages\" not in info:\n            node_packages[node] = defaultdict(lambda: \"UNKNOWN\")\n        else:\n            node_packages[node] = defaultdict(lambda: \"MISSING\")\n            for pkg, version in info[\"packages\"].items():\n                node_packages[node][pkg] = version\n                packages.add(pkg)\n\n    errs = []\n    for pkg in sorted(packages):\n        versions = set(\n            node_packages[node][pkg]\n            for node in nodes\n            if node != \"scheduler\" or pkg in scheduler_relevant_packages\n        )\n        if len(versions) <= 1:\n            continue\n        rows = [\n            (node_name, node_packages[node_name][pkg]) for node_name in nodes.keys()\n        ]\n        errs.append(\"%s\\n%s\" % (pkg, asciitable([\"\", \"version\"], rows)))\n    if errs:\n        return \"Mismatched versions found\\n\" \"\\n\" \"%s\" % (\"\\n\\n\".join(errs))\n    else:\n        return \"\"\n\n\nclass VersionMismatchWarning(Warning):\n    \"\"\"Indicates version mismatch between nodes\"\"\"\n", "path": "distributed/versions.py"}], "after_files": [{"content": "\"\"\" utilities for package version introspection \"\"\"\n\nfrom __future__ import print_function, division, absolute_import\n\nimport platform\nimport struct\nimport os\nimport sys\nimport importlib\n\n\nrequired_packages = [\n    (\"dask\", lambda p: p.__version__),\n    (\"distributed\", lambda p: p.__version__),\n    (\"msgpack\", lambda p: \".\".join([str(v) for v in p.version])),\n    (\"cloudpickle\", lambda p: p.__version__),\n    (\"tornado\", lambda p: p.version),\n    (\"toolz\", lambda p: p.__version__),\n]\n\noptional_packages = [\n    (\"numpy\", lambda p: p.__version__),\n    (\"lz4\", lambda p: p.__version__),\n    (\"blosc\", lambda p: p.__version__),\n]\n\n\n# only these scheduler packages will be checked for version mismatch\nscheduler_relevant_packages = set(pkg for pkg, _ in required_packages) | set(\n    [\"lz4\", \"blosc\"]\n)\n\n\n# notes to be displayed for mismatch packages\nnotes_mismatch_package = {\n    \"msgpack\": \"Variation is ok, as long as everything is above 0.6\",\n    \"lz4\": \"Variation is ok, but missing libraries are not\",\n    \"python\": \"Variation is sometimes ok, sometimes not. It depends on your workloads\",\n}\n\n\ndef get_versions(packages=None):\n    \"\"\"\n    Return basic information on our software installation, and our installed versions of packages.\n    \"\"\"\n    if packages is None:\n        packages = []\n\n    d = {\n        \"host\": get_system_info(),\n        \"packages\": get_package_info(\n            required_packages + optional_packages + list(packages)\n        ),\n    }\n\n    return d\n\n\ndef get_system_info():\n    (sysname, nodename, release, version, machine, processor) = platform.uname()\n    host = {\n        \"python\": \"%d.%d.%d.%s.%s\" % sys.version_info[:],\n        \"python-bits\": struct.calcsize(\"P\") * 8,\n        \"OS\": \"%s\" % sysname,\n        \"OS-release\": \"%s\" % release,\n        \"machine\": \"%s\" % machine,\n        \"processor\": \"%s\" % processor,\n        \"byteorder\": \"%s\" % sys.byteorder,\n        \"LC_ALL\": \"%s\" % os.environ.get(\"LC_ALL\", \"None\"),\n        \"LANG\": \"%s\" % os.environ.get(\"LANG\", \"None\"),\n    }\n\n    return host\n\n\ndef version_of_package(pkg):\n    \"\"\" Try a variety of common ways to get the version of a package \"\"\"\n    from .utils import ignoring\n\n    with ignoring(AttributeError):\n        return pkg.__version__\n    with ignoring(AttributeError):\n        return str(pkg.version)\n    with ignoring(AttributeError):\n        return \".\".join(map(str, pkg.version_info))\n    return None\n\n\ndef get_package_info(pkgs):\n    \"\"\" get package versions for the passed required & optional packages \"\"\"\n\n    pversions = [(\"python\", \".\".join(map(str, sys.version_info)))]\n    for pkg in pkgs:\n        if isinstance(pkg, (tuple, list)):\n            modname, ver_f = pkg\n        else:\n            modname = pkg\n            ver_f = version_of_package\n\n        if ver_f is None:\n            ver_f = version_of_package\n\n        try:\n            mod = importlib.import_module(modname)\n            ver = ver_f(mod)\n            pversions.append((modname, ver))\n        except Exception:\n            pversions.append((modname, None))\n\n    return dict(pversions)\n\n\ndef error_message(scheduler, workers, client, client_name=\"client\"):\n    from .utils import asciitable\n\n    client = client.get(\"packages\") if client else \"UNKNOWN\"\n    scheduler = scheduler.get(\"packages\") if scheduler else \"UNKNOWN\"\n    workers = {k: v.get(\"packages\") if v else \"UNKNOWN\" for k, v in workers.items()}\n\n    packages = set()\n    packages.update(client)\n    packages.update(scheduler)\n    for worker in workers:\n        packages.update(workers.get(worker))\n\n    errs = []\n    notes = []\n    for pkg in sorted(packages):\n        versions = set()\n        scheduler_version = (\n            scheduler.get(pkg, \"MISSING\") if isinstance(scheduler, dict) else scheduler\n        )\n        if pkg in scheduler_relevant_packages:\n            versions.add(scheduler_version)\n\n        client_version = (\n            client.get(pkg, \"MISSING\") if isinstance(client, dict) else client\n        )\n        versions.add(client_version)\n\n        worker_versions = set(\n            workers[w].get(pkg, \"MISSING\")\n            if isinstance(workers[w], dict)\n            else workers[w]\n            for w in workers\n        )\n        versions |= worker_versions\n\n        if len(versions) <= 1:\n            continue\n        if len(worker_versions) == 1:\n            worker_versions = list(worker_versions)[0]\n        elif len(worker_versions) == 0:\n            worker_versions = None\n\n        errs.append((pkg, client_version, scheduler_version, worker_versions))\n        if pkg in notes_mismatch_package.keys():\n            notes.append(f\"-  {pkg}: {notes_mismatch_package[pkg]}\")\n\n    if errs:\n        err_table = asciitable([\"Package\", client_name, \"scheduler\", \"workers\"], errs)\n        err_msg = f\"Mismatched versions found\\n\\n{err_table}\"\n        if notes:\n            err_msg += \"\\nNotes: \\n{}\".format(\"\\n\".join(notes))\n        return err_msg\n    else:\n        return \"\"\n\n\nclass VersionMismatchWarning(Warning):\n    \"\"\"Indicates version mismatch between nodes\"\"\"\n", "path": "distributed/versions.py"}]}