coady / multimethod

Multiple argument dispatching.
https://coady.github.io/multimethod
Other
277 stars 24 forks source link

dispatching on a sequence of callable breaks dispatch on callable #72

Closed francesco-ballarin closed 2 years ago

francesco-ballarin commented 2 years ago

Hi, in my application I would like to dispatch on a sequence of callable.

To this end, I slightly patched your test_callable test

diff --git a/tests/test_subscripts.py b/tests/test_subscripts.py
index 7a293e1..cf83f89 100644
--- a/tests/test_subscripts.py
+++ b/tests/test_subscripts.py
@@ -1,6 +1,6 @@
 import sys
 import pytest
-from typing import Callable, Generic, List, TypeVar
+from typing import Callable, Generic, List, Sequence, TypeVar
 from multimethod import multimethod, subtype, DispatchError

@@ -74,10 +74,15 @@ def test_callable():
     def _(arg: int):
         return 'int'

+    @func.register
+    def _(arg: Sequence[Callable[[bool], bool]]):
+        return arg[0].__name__ + "0"
+
     tp = subtype(func.__annotations__['arg'])
     assert not issubclass(tp.get_type(f), tp.get_type(g))
     assert issubclass(tp.get_type(g), tp.get_type(f))
     with pytest.raises(DispatchError):
         func(f)
     assert func(g) == 'g'
+    assert func([g]) == 'g0'
     assert func(h) is ...

Such new test fails with

_______________________________________________________ test_callable ________________________________________________________

    def test_callable():
        def f(arg: bool) -> int:
            ...

        def g(arg: int) -> bool:
            ...

        def h(arg) -> bool:
            ...

        @multimethod
        def func(arg: Callable[[bool], bool]):
            return arg.__name__

        @func.register
        def _(arg: Callable[..., bool]):
            return ...

        @func.register
        def _(arg: int):
            return 'int'

        @func.register
        def _(arg: Sequence[Callable[[bool], bool]]):
            return arg[0].__name__ + "0"

        tp = subtype(func.__annotations__['arg'])
        assert not issubclass(tp.get_type(f), tp.get_type(g))
        assert issubclass(tp.get_type(g), tp.get_type(f))
        with pytest.raises(DispatchError):
            func(f)
>       assert func(g) == 'g'

test_subscripts.py:86: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
/usr/local/lib/python3.10/dist-packages/multimethod/__init__.py:310: in __call__
    func = self[tuple(func(arg) for func, arg in zip(self.type_checkers, args))]
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = {(<class 'multimethod.typing.Callable[[bool], bool]'>,): <function test_callable.<locals>.func at 0x7fcd6081d3f0>, (<c...'multimethod.typing.Sequence[typing.Callable[[bool], bool]]'>,): <function test_callable.<locals>._ at 0x7fcd6081d630>}
types = (<class 'function'>,)

    def __missing__(self, types: tuple) -> Callable:
        """Find and cache the next applicable method of given types."""
        self.evaluate()
        if types in self:
            return self[types]
        groups = collections.defaultdict(list)
        for key in self.parents(types):
            if key.callable(*types):
                groups[types - key].append(key)
        keys = groups[min(groups)] if groups else []
        funcs = {self[key] for key in keys}
        if len(funcs) == 1:
            return self.setdefault(types, *funcs)
        msg = f"{self.__name__}: {len(keys)} methods found"  # type: ignore
>       raise DispatchError(msg, types, keys)
E       multimethod.DispatchError: ('func: 0 methods found', (<class 'function'>,), [])

/usr/local/lib/python3.10/dist-packages/multimethod/__init__.py:304: DispatchError
================================================== short test summary info ===================================================
FAILED test_subscripts.py::test_callable - multimethod.DispatchError: ('func: 0 methods found', (<class 'function'>,), [])
================================================ 1 failed, 3 passed in 0.08s =================================================

Notice that the failing line is the call func(g) and not my newly added call func([g]).

My python version is 3.10, and I am on the latest pypi release with a manual application of 857e4e98299d55fe7097238eae7179287248913f

francesco-ballarin commented 2 years ago

OK, that's my mistake, but still it does not justify the error above.

I've now realized that the failure occurs randomly: approximately 50% of time I run it the test pass (when, based on your comment above, apparently should fail due to wrong argument type in func([g]), the other 50% it fails with the error in the opening post.

I am now on latest commit.

coady commented 2 years ago

Correction: your example should work. There was a bug in the type checker, because the builtin Callable is not considered a subclass of the builtin function type.

francesco-ballarin commented 2 years ago

Thanks, the latest commit fixes my issue.