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

How to use @dispatch with @staticmethod inside a class? #35

Closed
Imipenem opened this issue Dec 4, 2021 · 9 comments
Closed

How to use @dispatch with @staticmethod inside a class? #35

Imipenem opened this issue Dec 4, 2021 · 9 comments
Labels

Comments

@Imipenem
Copy link

Imipenem commented Dec 4, 2021

Hey,

consider the following code snippet:

   @dispatch
    def encode(
        data: MyDataType,
        autodetect: bool=False,
        encodings:Optional[Dict[str, List[str]]] =None,
    ) -> Optional[MyDataType]:
        """Encode a MyDataType object"""
        ... do soemthing here

and

    @staticmethod
    def encode(
        data: MyOtherDataType,
        autodetect: Optional[Dict] = None,
        encodings: Optional[Dict[str, Dict[str, List[str]]]] = None,
    ) -> None:
 .... do something here

So I have two staticmethods inside a class and I want to use dispatch on them.
When I try to run the code above it always fails with:

/my/home/dir/plum/function.py:226 in plum.function.ClassFunction.__call__    │
│                                                                                           │
│ [Errno 2] No such file or directory: 'my/home/dir/plum/function.py'  (What is this even trying to do here??)
...
ResolutionError: Promise `Promise()` was not kept.

I read in your docs, that the dispatch decorator should be the outermost decorator. But doing so with staticmethod, will also result in an error indicating that the dispatch operator does not get a callable object (but a staticmethod object) passed.

Any ideas whats going on here and how to resolve this?
Many thanks ;)

@PhilipVinc
Copy link
Collaborator

Can you provide a short script that reproduces the error?

@Imipenem
Copy link
Author

Imipenem commented Dec 4, 2021

@PhilipVinc

from typing import Optional, Dict, List
from plum import dispatch

class MyClass:
    @staticmethod
    @dispatch
    def encode(
        data: str,
        autodetect: bool=False,
        encodings:Optional[Dict[str, List[str]]] =None,
    ) -> Optional[str]:
        print("First function stuff going on here")
        return None

    @staticmethod
    @dispatch
    def encode(
        data: bool,
        autodetect: Optional[Dict] = None,
        encodings: Optional[Dict[str, Dict[str, List[str]]]] = None,
    ) -> None:
        print("Other function stuff happening here")


if __name__ == '__main__':
    MyClass.encode("CallFirstFun", True, {"some_data":["some values"]})

@PhilipVinc
Copy link
Collaborator

It looks like that

@dispatch
@staticmethod
def encode(...)

does not work because inspect.signature cannot inspect a static method during construction of the class. See below:

import plum
from inspect import signature
from typing import Callable


class Test:
	@staticmethod
	def test(arg):
		return arg

print(signature(Test.test)) #(arg)

def decorator(fun):
	print(signature(fun))
	return fun

class Test2:
	@decorator
	@staticmethod
	def test(arg):
		return arg

the first printing works, but when defining Test2 using the decorator we get the error

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in Test
  File "<stdin>", line 2, in decorator
  File "/Users/filippovicentini/.pyenv/versions/3.9.7/lib/python3.9/inspect.py", line 3111, in signature
    return Signature.from_callable(obj, follow_wrapped=follow_wrapped)
  File "/Users/filippovicentini/.pyenv/versions/3.9.7/lib/python3.9/inspect.py", line 2860, in from_callable
    return _signature_from_callable(obj, sigcls=cls,
  File "/Users/filippovicentini/.pyenv/versions/3.9.7/lib/python3.9/inspect.py", line 2259, in _signature_from_callable
    raise TypeError('{!r} is not a callable object'.format(obj))
TypeError: <staticmethod object at 0x108865250> is not a callable object

I suspect this is a python bug that should be reported upstream...

Inverting the order, (@staticmethod -> @dispatch) is not supported. I'm not sure how hard it would be to support it properly

cc @wesselb

@Imipenem
Copy link
Author

Imipenem commented Dec 5, 2021

Thanks for the explanation; it's definitely interesting that a staticmethod
isn't a callable object, seems counterintuitive.

In my case, I think we will completely remove the classes (since they really do not have a deeper purpose than being a "container").

Anyways, would be interesting, whether it "could be supported" or not.

@PhilipVinc
Copy link
Collaborator

If we could access the wrapped method we could special case it, but it seems that if you do

def test(arg):
    return arg

tm = staticmethod(test)

you can't get out whatever is wrapped inside of the static method.

If you manage to find how to get it out (I suspect it might be possible with the C api) then it would be easy to support it.

@wesselb
Copy link
Member

wesselb commented Dec 6, 2021

Hey both, I’m currently away on holidays, but I’ll get back to this as soon as I’m back. On a first thought, perhaps this SO answer is useful: could we extract the function by using the descriptor protocol?

@wesselb
Copy link
Member

wesselb commented Dec 9, 2021

It seems that you can extract the method as follows:

In [1]: def f(x):
   ...:     return x
   ...:

In [2]: f_static = staticmethod(f)

In [3]: f_static.__get__(object())
Out[3]: <function __main__.f(x)>

In [4]: f
Out[4]: <function __main__.f(x)>

In [6]: f_static.__get__(object()) is f
Out[6]: True

This means that we could potentially treat it specially, as @PhilipVinc suggests.

However, it appears that @staticmethod and @classmethod already work if the order of the decorators is reversed.

from plum import dispatch


class A:
    @staticmethod
    @dispatch
    def f(x: int):
        return 1

    @staticmethod
    @dispatch
    def f(x: str):
        return "1"

    @classmethod
    @dispatch
    def g(cls, x: int):
        return cls, 1

    @classmethod
    @dispatch
    def g(cls, x: str):
        return cls, "1"

    @dispatch
    def _(self):
        pass
In [14]: A.g(1)
Out[14]: (__main__.A, 1)

In [15]: A.g("1")
Out[15]: (__main__.A, '1')

In [16]: A.f(1)
Out[16]: 1

In [17]: A.f("1")
Out[17]: '1'

In [18]: A.g(1)
Out[18]: (__main__.A, 1)

In [19]: A.g("1")
Out[19]: (__main__.A, '1')

The reason why it didn't work for @Imipenem is that there is some complicated logic going on to enable the functions to hold a reference to the class, which requires the class to have at least one function which has @dispatch as the outermost decorator, which is what the dummy method _ does. Instead of the dummy method, I imagine that we could implement a decorator so that

@dispatchable
class A:
    @staticmethod
    @dispatch
    def f(x: int):
        return 1

would work. Perhaps there is a better way?

@wesselb
Copy link
Member

wesselb commented Dec 9, 2021

I've added a section to the README. A better solution would be preferable, of course.

@Imipenem
Copy link
Author

I see, many thanks for the detailed explanation. Thanks also for the README section about this.

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

No branches or pull requests

3 participants