Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dict ambiguity #88

Open
hodgespodge opened this issue Jun 26, 2023 · 5 comments
Open

Dict ambiguity #88

hodgespodge opened this issue Jun 26, 2023 · 5 comments

Comments

@hodgespodge
Copy link
Contributor

I have not worked with covariant typing before so forgive if the following is ignorance on my part:

I assume the following code should be unambiguous. It fails due to ambiguity (tested with Python 3.11.3, plum-dispatch 2.1.1, and beartype 0.14.1 on Windows 10) :

from plum import dispatch
from typing import Dict

import plum
import beartype
import sys

print("plum version:",plum.__version__)
print("beartype version:",beartype.__version__)
print("python version:",sys.version)

class A:
    pass

class B:
    
    @dispatch
    def my_func(self, dictionary: Dict[int, int]) -> None:
        print("calling my_func(self, dict: Dict[int, int])")

    @dispatch
    def my_func(self, dictionary: Dict[str, str]) -> None:
        print("calling my_func(self, dict: Dict[str, str])")

    @dispatch
    def my_func(self, dictionary: Dict[A, A]) -> None:
        print("calling my_func(self, dict: Dict[A, A])")

b = B()
b.my_func({1: 2})
b.my_func({"1": "2"})
b.my_func({A(): A()})

plum version: 2.1.1
beartype version: 0.14.1
python version: 3.11.3 (tags/v3.11.3:f3909b8, Apr 4 2023, 23:49:59) [MSC v.1934 64 bit (AMD64)]
Traceback (most recent call last):
File "C:\Users\Shodges\AppData\Roaming\Python\Python311\site-packages\plum\function.py", line 387, in call
method, return_type = self._cache[types]
~~~~~~~~~~~^^^^^^^
KeyError: (<class 'main.B'>, <class 'dict'>)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "C:\Users\Shodges\AppData\Roaming\Python\Python311\site-packages\plum\function.py", line 320, in resolve_method
signature = self._resolver.resolve(target)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Shodges\AppData\Roaming\Python\Python311\site-packages\plum\resolver.py", line 182, in resolve
raise AmbiguousLookupError(
plum.resolver.AmbiguousLookupError: (<__main__.B object at 0x0000020D0C45F350>, {1: 2}) is ambiguous among the following:
Signature(typing.Any, dict[int, int], return_type=NoneType, implementation=<function B.my_func at 0x0000020D0BE365C0>) (precedence: 0)
Signature(typing.Any, dict[str, str], return_type=NoneType, implementation=<function B.my_func at 0x0000020D0C4711C0>) (precedence: 0)
Signature(typing.Any, dict[main.A, main.A], return_type=NoneType, implementation=<function B.my_func at 0x0000020D0C471260>) (precedence: 0)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
File "C:\Users\Shodges\Desktop\test5.py", line 30, in
b.my_func({1: 2})
File "C:\Users\Shodges\AppData\Roaming\Python\Python311\site-packages\plum\function.py", line 459, in call
return self._f(self._instance, *args, **kw_args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Shodges\AppData\Roaming\Python\Python311\site-packages\plum\function.py", line 390, in call
method, return_type = self.resolve_method(args, types)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\Shodges\AppData\Roaming\Python\Python311\site-packages\plum\function.py", line 325, in resolve_method
raise self._enhance_exception(e) # Specify this function.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
plum.resolver.AmbiguousLookupError: For function my_func of __main__.B, (<__main__.B object at 0x0000020D0C45F350>, {1: 2}) is ambiguous among the following:
Signature(typing.Any, dict[int, int], return_type=NoneType, implementation=<function B.my_func at 0x0000020D0BE365C0>) (precedence: 0)
Signature(typing.Any, dict[str, str], return_type=NoneType, implementation=<function B.my_func at 0x0000020D0C4711C0>) (precedence: 0)
Signature(typing.Any, dict[main.A, main.A], return_type=NoneType, implementation=<function B.my_func at 0x0000020D0C471260>) (precedence: 0)

Thanks for any clarification. I'm enjoying the library otherwise!

@wesselb
Copy link
Member

wesselb commented Jun 26, 2023

Hey @hodgespodge! Thanks for opening an issue. :)

I believe the ambiguity arises from the following:

In [1]: from beartype.door import is_bearable

In [2]: is_bearable({"1": "2"}, dict[int, int])
Out[2]: True

This is not right. I will open an issue on Beartype. EDIT: Done.

@wesselb
Copy link
Member

wesselb commented Jun 27, 2023

@hodgespodge Deep type checking of dicts is currently not supported by Beartype, see this issue and this part of the Beartype docs. However, @leycec is working hard on Beartype, and we might be lucky that this will be supported in the not too distant future. Woo!

Unfortunately, this means that there is no current solution. :( You might have to go for something hacky like this:

from typing import List, Tuple

from plum import dispatch

@dispatch
def _f(x: List[Tuple[str, str]]):
    x = dict(x)
    print("dict[str, str]")

@dispatch
def _f(x: List[Tuple[int, int]]):
    x = dict(x)
    print("dict[int, int]")

@dispatch
def f(x: dict):
    return _f(list(x.items()))

f({1: 1})

f({"1": "1"})

You could use some decorators and other syntactic sugar to make this look nicer. If you're really into it, you could even use some magic to automatically convert Dict type hints in this way, so that the above pattern would be automatically applied.

I will keep this in mind and update this issue as soon as a better solution is available.

@hodgespodge
Copy link
Contributor Author

@wesselb Thanks for investigating and getting back so quickly. I'll patiently look forward to the fix.

Like I said, I'm really enjoying the library otherwise!

@leycec
Copy link
Member

leycec commented Jul 3, 2023

I have been summoned. On behalf of all @beartype versions everywhere, lead @beartype guy @leycec is here to apologize for our present lack of deep type-checking of dictionaries. To summarize the current state of @beartype:

  • @beartype 0.15.0 (...to surely be released in a week or two, surely) will provide support for import hooks. Let's not ask what that means.
  • @beartype 0.16.0 (...to surely be released by September or October, surely) will provide support for deep type-checking of dictionaries. This is what everyone wants and deserves here.

Alternately, anyone with a pressing need to deeply type-check dictionaries now can do so via our existing beartype.vale API: e.g.,

from beartype import beartype
from beartype.typing import Annotated
from beartype.vale import Is

# Type hint matching a dictionary mapping from strings to... other strings.
# Use "dict_of_strs_to_strs" instead of "dict[str, str]" for profit and fun.
dict_of_strs_to_strs = Annotated[
    dict, Is[lambda mapping:
        isinstance(next(iter(mapping.keys())), str) and
        isinstance(next(iter(mapping.values())), str)
]]

@beartype
def muh_func(muh_dict: dict_of_strs_to_strs) -> str:
    return (muh_dict['good key?'] if 'good key?' in muh_dict else 'bad key!')

# Prints: "...is good". Feel the good Summer vibrations, everyone -- except
# for those poor bastards in Australia and New Zealand. Pity them as they
# celebrate Summer Christmas with Aussie Santa Claus and his Kiwi reindeer.
print(muh_func({'good key?': '...is good'}))

Tested and working. Punch a duck or just use beartype.vale. The power is yours. ✊

@wesselb
Copy link
Member

wesselb commented Jul 3, 2023

Yesss, it is the one and only @leycec! Your tentative schedule of releases sound really promising. I will be looking forward to them very much. :)

And thanks for pointing out the option to use beartype.vale. That's actually not a bad option at all and provides a very reasonable fix until 0.16.0 comes around. Wooo!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants