Repository: coady/multimethod

Issue:
Overloading with optional parameters
Hi,

The following example produces an error:
`
class A:
    @multimethod
    def m(self, c: int, ii: Optional[int] = None):
        print('1', c, ii)

    @multimethod
    def m(self, ff: float):
        print('2', ff)


obj = A()
obj.m(20)
`

when calling `obj.m(20)`:
`
  File "/usr/local/lib/python3.8/dist-packages/multimethod/__init__.py", line 184, in __call__
    return self[tuple(map(self.get_type, args))](*args, **kwargs)
  File "/usr/local/lib/python3.8/dist-packages/multimethod/__init__.py", line 180, in __missing__
    raise DispatchError(msg, types, keys)
multimethod.DispatchError: ('a: 0 methods found', (<class '__main__.A'>, <class 'int'>), [])

Process finished with exit code 1
`

Is there a way around it?

Discussion:

Patch:
diff --git a/README.md b/README.md
index 14199c2..0bb2db6 100644
--- a/README.md
+++ b/README.md
@@ -76,7 +76,7 @@ Dispatch resolution details:
 * If the `issubclass` relation is ambiguous,
 [mro](https://docs.python.org/3/library/stdtypes.html?highlight=mro#class.mro) position is used as a tie-breaker.
 * If there are still ambiguous methods - or none - a custom `TypeError` is raised.
-* Additional `*args` or `**kwargs` may be used when calling, but won't affect the dispatching.
+* Default and keyword arguments may be used, but won't affect the dispatching.
 * A skipped annotation is equivalent to `: object`, which implicitly supports methods by leaving `self` blank.
 * If no types are specified, it will inherently match all arguments.
 
@@ -151,6 +151,7 @@ dev
 
 * Postponed evaluation of nested annotations
 * Variable-length tuples of homogeneous type
+* Ignore default and keyword arguments
 
 1.4
 
diff --git a/multimethod/__init__.py b/multimethod/__init__.py
index 1a11644..80d330b 100644
--- a/multimethod/__init__.py
+++ b/multimethod/__init__.py
@@ -19,13 +19,17 @@ def groupby(func: Callable, values: Iterable) -> dict:
 
 
 def get_types(func: Callable) -> tuple:
-    """Return evaluated type hints in order."""
+    """Return evaluated type hints for positional required parameters in order."""
     if not hasattr(func, '__annotations__'):
         return ()
-    annotations = dict(typing.get_type_hints(func))
-    annotations.pop('return', None)
-    params = inspect.signature(func).parameters
-    return tuple(annotations.pop(name, object) for name in params if annotations)
+    type_hints = typing.get_type_hints(func)
+    positionals = {inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD}
+    annotations = [
+        type_hints.get(param.name, object)
+        for param in inspect.signature(func).parameters.values()
+        if param.default is param.empty and param.kind in positionals
+    ]  # missing annotations are padded with `object`, but trailing objects are unnecessary
+    return tuple(itertools.dropwhile(lambda cls: cls is object, reversed(annotations)))[::-1]
 
 
 class DispatchError(TypeError):


Test patch:
diff --git a/tests/test_dispatch.py b/tests/test_dispatch.py
index 8f71386..a628790 100644
--- a/tests/test_dispatch.py
+++ b/tests/test_dispatch.py
@@ -1,6 +1,7 @@
+import sys
 from collections.abc import Iterable
 import pytest
-from multimethod import multidispatch, signature, DispatchError
+from multimethod import get_types, multidispatch, signature, DispatchError
 
 
 def test_signature():
@@ -74,3 +75,12 @@ def test_cls():
         cls.method('', '')
     cls.method[object, Iterable] = cls.method[Iterable, object]
     assert cls.method('', '') == 'left'
+
+
+def test_arguments():
+    def func(a, b: int, c: int, d, e: int = 0, *, f: int):
+        pass
+
+    if sys.version_info >= (3, 8):
+        exec("def func(a, b: int, /, c: int, d, e: int = 0, *, f: int): pass")
+    assert get_types(func) == (object, int, int)