Repository: coady/multimethod

Issue:
TypeError when one of the type hints is type or another metaclass
Using `type` (or any other subclass of `type`, e.g. a custom metaclass) as the type hint for one of the parameters of a function of a multimethod makes the dispatch process raise a `TypeError` when calling `type.mro()` (which expects the type object as an argument).

## Example

```python
@multimethod
def f(x: type):
    print("success")
```

### Expected

```python
>>> f(int)
success
```


### Actual

```python
>>> f(int)
TypeError: unbound method type.mro() needs an argument
```

Traceback:

<details>

```
Traceback (most recent call last):
  File "C:\Users\Paolo\Code\JB-PycharmProjects\LoveLetter\venv\lib\site-packages\IPython\core\interactiveshell.py", line 3418, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-34-0374cd2152b2>", line 1, in <module>
    f(int)
  File "C:\Users\Paolo\Code\JB-PycharmProjects\LoveLetter\venv\lib\site-packages\multimethod\__init__.py", line 184, in __call__
    return self[tuple(map(self.get_type, args))](*args, **kwargs)
  File "C:\Users\Paolo\Code\JB-PycharmProjects\LoveLetter\venv\lib\site-packages\multimethod\__init__.py", line 184, in __call__
    return self[tuple(map(self.get_type, args))](*args, **kwargs)
  File "C:\Users\Paolo\Code\JB-PycharmProjects\LoveLetter\venv\lib\site-packages\multimethod\__init__.py", line 174, in __missing__
    groups = groupby(signature(types).__sub__, self.parents(types))
  File "C:\Users\Paolo\Code\JB-PycharmProjects\LoveLetter\venv\lib\site-packages\multimethod\__init__.py", line 17, in groupby
    groups[func(value)].append(value)
  File "C:\Users\Paolo\Code\JB-PycharmProjects\LoveLetter\venv\lib\site-packages\multimethod\__init__.py", line 104, in __sub__
    return tuple(mro.index(cls if cls in mro else object) for mro, cls in zip(mros, other))
  File "C:\Users\Paolo\Code\JB-PycharmProjects\LoveLetter\venv\lib\site-packages\multimethod\__init__.py", line 104, in <genexpr>
    return tuple(mro.index(cls if cls in mro else object) for mro, cls in zip(mros, other))
  File "C:\Users\Paolo\Code\JB-PycharmProjects\LoveLetter\venv\lib\site-packages\multimethod\__init__.py", line 103, in <genexpr>
    mros = (subclass.mro() for subclass in self)
TypeError: unbound method type.mro() needs an argument
```

</details>

## Cause

Methods of metaclasses like `type`, e.g. `.mro()` expect an explicit first argument which is the actual type object being used.


Discussion:

Patch:
diff --git a/README.md b/README.md
index 4da3048..461c589 100644
--- a/README.md
+++ b/README.md
@@ -140,6 +140,7 @@ dev
 * Ignore default and keyword arguments
 * Resolved ambiguous `Union` types
 * Fixed an issue with name collision when defining a multimethod
+* Resolved dispatch errors when annotating parameters with meta-types such as `type`
 
 1.4
 
diff --git a/multimethod/__init__.py b/multimethod/__init__.py
index 22ffff3..63cb6a1 100644
--- a/multimethod/__init__.py
+++ b/multimethod/__init__.py
@@ -99,7 +99,7 @@ def distance(cls, subclass: type) -> int:
     """Return estimated distance between classes for tie-breaking."""
     if getattr(cls, '__origin__', None) is typing.Union:
         return min(distance(arg, subclass) for arg in cls.__args__)
-    mro = subclass.mro()
+    mro = type.mro(subclass)
     return mro.index(cls if cls in mro else object)
 
 


Test patch:
diff --git a/tests/test_methods.py b/tests/test_methods.py
index 8c572f8..0409431 100644
--- a/tests/test_methods.py
+++ b/tests/test_methods.py
@@ -1,3 +1,5 @@
+import enum
+
 import pytest
 from typing import Any, AnyStr, Dict, Iterable, List, Tuple, TypeVar, Union
 from multimethod import (
@@ -85,6 +87,13 @@ def test_signature():
     assert signature([List[int]]) - signature([list])
     assert signature([list]) - signature([List[int]]) == (1,)
 
+    # with metaclasses:
+    assert signature([type]) - signature([type]) == (0,)
+    assert signature([type]) - signature([object]) == (1,)
+    # using EnumMeta because it is a standard, stable, metaclass
+    assert signature([enum.EnumMeta]) - signature([object]) == (2,)
+    assert signature([Union[type, enum.EnumMeta]]) - signature([object]) == (1,)
+
 
 def test_get_type():
     method = multimethod(lambda: None)
@@ -229,6 +238,30 @@ def test_ellipsis():
         func(((0, 1.0),))
 
 
+def test_meta_types():
+    @multimethod
+    def f(x):
+        return "object"
+
+    @f.register
+    def f(x: type):
+        return "type"
+
+    @f.register
+    def f(x: enum.EnumMeta):
+        return "enum"
+
+    @f.register
+    def f(x: enum.Enum):
+        return "member"
+
+    dummy_enum = enum.Enum("DummyEnum", names="SPAM EGGS HAM")
+    assert f(123) == "object"
+    assert f(int) == "type"
+    assert f(dummy_enum) == "enum"
+    assert f(dummy_enum.EGGS) == "member"
+
+
 def test_name_shadowing():
     # an object with the same name appearing previously in the same namespace
     temp = 123  # noqa