Repository: coady/multimethod

Issue:
Collect docstring from registered functions
Hi! I just found this library and really like it so far, but I miss having the docstring available in the tooltip when calling a multimethod in a Jupyter Notebook.

Discussion:
coady: The build failure is because f-strings were added in 3.6.  Maybe it's time to drop 3.5 support anyway; many projects have already.
bagibence: Or do you want me to rewrite it and use another way of formatting?

Patch:
diff --git a/.travis.yml b/.travis.yml
index da158e0..87b9f79 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,7 +1,6 @@
 language: python
 
 python:
-  - 3.5
   - 3.6
   - 3.7
   - 3.8
diff --git a/README.md b/README.md
index 753a640..e2e0e4e 100644
--- a/README.md
+++ b/README.md
@@ -147,6 +147,9 @@ class Foo:
 ```
 
 # Changes
+dev
+* Python >=3.6 required
+
 1.3
 * Python 3 required
 * Support for subscripted ABCs
diff --git a/docs/examples.ipynb b/docs/examples.ipynb
index a6f77ed..1128cc9 100644
--- a/docs/examples.ipynb
+++ b/docs/examples.ipynb
@@ -168,6 +168,53 @@
    "source": [
     "wait(0.5, asyncio.sleep, 0.01)"
    ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "## typing subscripts\n",
+    "Provisional support for type hints with subscripts."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "import bisect\n",
+    "import random\n",
+    "from typing import Dict\n",
+    "\n",
+    "@multimethod\n",
+    "def samples(weights: Dict):\n",
+    "    \"\"\"Generate weighted random samples using bisection.\"\"\"\n",
+    "    keys = list(weights)\n",
+    "    totals = list(itertools.accumulate(weights.values()))\n",
+    "    values = [total / totals[-1] for total in totals]\n",
+    "    while True:\n",
+    "        yield keys[bisect.bisect_right(values, random.random())]\n",
+    "\n",
+    "@multimethod\n",
+    "def samples(weights: Dict[object, int]):\n",
+    "    \"\"\"Generate weighted random samples more efficiently.\"\"\"\n",
+    "    keys = list(itertools.chain.from_iterable([key] * weights[key] for key in weights))\n",
+    "    while True:\n",
+    "        yield random.choice(keys)\n",
+    "\n",
+    "weights = {'a': 1, 'b': 2, 'c': 3}\n",
+    "next(samples(weights))"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "weights = {'a': 1.0, 'b': 2.0, 'c': 3.0}\n",
+    "next(samples(weights))"
+   ]
   }
  ],
  "metadata": {
@@ -187,7 +234,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.7.5"
+   "version": "3.7.6-final"
   }
  },
  "nbformat": 4,
diff --git a/multimethod.py b/multimethod.py
index a93d092..711a988 100644
--- a/multimethod.py
+++ b/multimethod.py
@@ -163,7 +163,7 @@ class multimethod(dict):
         funcs = {self[key] for key in keys}
         if len(funcs) == 1:
             return self.setdefault(types, *funcs)
-        msg = "{}: {} methods found".format(self.__name__, len(keys))  # type: ignore
+        msg = f"{self.__name__}: {len(keys)} methods found"  # type: ignore
         raise DispatchError(msg, types, keys)
 
     def __call__(self, *args, **kwargs):
@@ -179,6 +179,31 @@ class multimethod(dict):
         while self.pending:
             func = self.pending.pop()
             self[get_types(func)] = func
+            
+    @property
+    def __doc__(self):
+        docs = []
+        if any([f.__doc__ is not None for f in set(self.values())]):
+            docs.append('Signatures with a docstring:')
+
+        other = []
+        for func in set(self.values()):
+            if func.__doc__:
+                s = f'{func.__name__}{inspect.signature(func)}'
+                s += '\n' + '-' * len(s)
+                s += '\n'.join([line.strip() for line in func.__doc__.split('\n')])
+                docs.append(s)
+            else:
+                other.append(f'{func.__name__}{inspect.signature(func)}')
+                
+        if other:
+            docs.append('Signatures without a docstring:\n    ' + '\n    '.join(other))
+            
+        return '\n\n'.join(docs)
+
+    @__doc__.setter
+    def __doc__(self, value):
+        pass
 
 
 class multidispatch(multimethod):
diff --git a/setup.py b/setup.py
index 06330be..60247d9 100644
--- a/setup.py
+++ b/setup.py
@@ -14,7 +14,7 @@ setup(
     license='Apache Software License',
     py_modules=['multimethod'],
     extras_require={'docs': ['m2r', 'nbsphinx', 'jupyter']},
-    python_requires='>=3.5',
+    python_requires='>=3.6',
     tests_require=['pytest-cov'],
     keywords='multiple dispatch multidispatch generic functions methods overload',
     classifiers=[
@@ -23,7 +23,6 @@ setup(
         'License :: OSI Approved :: Apache Software License',
         'Operating System :: OS Independent',
         'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.5',
         'Programming Language :: Python :: 3.6',
         'Programming Language :: Python :: 3.7',
         'Programming Language :: Python :: 3.8',


Test patch:
diff --git a/tests/test_docstring.py b/tests/test_docstring.py
new file mode 100644
index 0000000..a4b565a
--- /dev/null
+++ b/tests/test_docstring.py
@@ -0,0 +1,24 @@
+from multimethod import multimethod
+
+
+@multimethod
+def foo(bar: int):
+    """
+    Argument is an integer
+    """
+    pass
+
+@multimethod
+def foo(bar: str):
+    """
+    Argument is a string
+    """
+    pass
+
+
+def test_docstring():
+    """
+    Test if multimethod collects its children's docstrings
+    """
+    assert "Argument is an integer" in foo.__doc__
+    assert "Argument is a string" in foo.__doc__