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

multimeta-like metaclass for plum #134

Open
sylvorg opened this issue Mar 17, 2024 · 12 comments
Open

multimeta-like metaclass for plum #134

sylvorg opened this issue Mar 17, 2024 · 12 comments

Comments

@sylvorg
Copy link

sylvorg commented Mar 17, 2024

Hello!

Would it be possible to add a multimeta-like metaclass for plum, pulled directly from the multimethod source code?

I have a working example below:

from plum import dispatch


def plume(*args, **kwargs):
    def wrapper(func):
        func.__plume__ = Plume(args, kwargs)
        return func

    return wrapper


# Adapted From: https://github.com/coady/multimethod/blob/main/multimethod/__init__.py#L488-L498
class plumeta(type):
    """Convert all callables in namespace to Dispatchers."""

    class __prepare__(dict):
        def __init__(self, *args):
            self.__dispatcher__ = Dispatcher()
            super().__setitem__("__dispatcher__", self.__dispatcher__)

        def __setitem__(self, key, value):
            if callable(value):
                args, kwargs = getattr(value, "__plume__", (tuple(), dict()))
                value = getattr(self.get(key), "dispatch", self.__dispatcher__)(
                    value, *args, **kwargs
                )
            super().__setitem__(key, value)


class Test(metaclass=plumeta):
    def a(self, c):
        print(f"{c} is an unknown type in 'a'!")

    def a(self):
        print("Nothing has been provided to 'a'!")

    def a(self, c: str):
        print(f"{c} is a string in 'a'!")

    def a(self, c: int):
        print(f"{c} is an integer in 'a'!")

    def a(self, c: str, d: int):
        print(f"{c} is a string and {d} is an integer in 'a'!")

    def b(self, c):
        print(f"{c} is an unknown type in 'b'!")

    def b(self):
        print("Nothing has been provided to 'b'!")

    def b(self, c: int):
        print(f"{c} is a integer in 'b'!")

    def b(self, c: str):
        print(f"{c} is a string in 'b'!")

    def b(self, c: int, d: str):
        print(f"{c} is an integer and {d} is a string in 'b'!")


test = Test()
test.a()  # Nothing has been provided to 'a'!
test.a(lambda x: x)  # <function <lambda> at 0x000000000000> is an unknown type in 'a'!
test.a("a")  # a is a string in 'a'!
test.a(1)  # 1 is an integer in 'a'!
test.a("a", 1)  # a is a string and 1 is an integer in 'a'!
test.b()  # Nothing has been provided to 'b'!
test.b(lambda x: x)  # <function <lambda> at 0x000000000000> is an unknown type in 'b'!
test.b(2)  # 2 is an integer in 'b'!
test.b("b")  # b is a string in 'b'!
test.b(2, "b")  # 2 is an integer and b is a string in 'b'!

Of course, I'm assuming there are other tests I'd have to run to ensure the same result, but otherwise, would this work?

Thank you kindly for your consideration on the matter!

@wesselb
Copy link
Member

wesselb commented Mar 20, 2024

Hey @sylvorg!

Thanks for opening an issue. :) This sounds very reasonable.

I’m currently away, but should be back soon. I will get back to you some time next week.

@sylvorg
Copy link
Author

sylvorg commented Mar 20, 2024

Got it; no worries, and I look forward to your return!

@wesselb
Copy link
Member

wesselb commented Mar 25, 2024

Hey @sylvorg!

I'm back again. I think such a metaclass could be useful and would be a very sensible addition to the library. :)

Unfortunately, I don't have the capacity to do this myself on the short term. I am a little overloaded at the moment. :( However, if you would like to have a stab at this, contributions are very welcome! Otherwise, I'll put it on the TODO list and will implement this at a later point in time.

@sylvorg
Copy link
Author

sylvorg commented Mar 25, 2024

I can probably put together a PR, but I'm not exactly sure where to put the class; should I just put it in __init__.py, or create a separate file just for it? And how should I credit the original author(s)? Should I just ping them in this issue?

@wesselb
Copy link
Member

wesselb commented Apr 20, 2024

@sylvorg, very sorry for the super late reply. Work has been very busy, meaning that I have less-than-usual capacity for side projects. :(

I think a separate file would make sense. Perhaps we can call the class DispatchMeta in plum/dispatchmeta.py to keep consistent with the current naming and capitalisation of things?

And how should I credit the original author(s)? Should I just ping them in this issue?

A clear remark in the docstring would suffice, I think. Pinging them in this issue might be nice too. :)

@sylvorg
Copy link
Author

sylvorg commented Apr 27, 2024

I'll ask @coady now; hopefully they aren't too irritated by this request, considering I disturbed them quite a bit before coming here! 😅

How does something like the following look?

from collections import namedtuple
from plum import Dispatcher


Plume = namedtuple("Plume", "args,kwargs")


def plume(*args, **kwargs):
    def wrapper(func):
        func.__plume__ = Plume(args, kwargs)
        return func

    return wrapper

# Adapted From: https://github.com/coady/multimethod/blob/main/multimethod/__init__.py#L488-L498
class DispatchMeta(type):
    """Convert all callables in namespace to Dispatchers."""

    class __prepare__(dict):
        def __init__(self, *args):
            self.__dispatcher__ = Dispatcher()
            super().__setitem__("__dispatcher__", self.__dispatcher__)

        def __setitem__(self, key, value):
            if callable(value):
                args, kwargs = getattr(value, "__plume__", (tuple(), dict()))
                if not kwargs.get("disabled", False):
                    value = getattr(self.get(key), "dispatch", self.__dispatcher__)(
                        value, *args, **kwargs
                    )
            super().__setitem__(key, value)

The plume function (name subject to change) allows users to set settings like the precedence, while the disabled keyword argument prevents the specified functions from being dispatched, to prevent unwanted recursion to parent classes from child classes, for example.

@wesselb
Copy link
Member

wesselb commented Apr 27, 2024

@sylvorg I think that looks great!! What would you think of something like this:

from plum import Dispatcher, configure_dispatch

dispatch = Dispatcher()


class MyClass(metaclass=dispatch.meta):
    @configure_dispatch(precedence=1)
    def method(self, x):
        return x

Here dispatch.meta creates a metaclass that uses dispatch as the dispatcher. plume is a funny one :D, but perhaps configure_dispatch is a little clearer 😅.

@sylvorg
Copy link
Author

sylvorg commented Apr 27, 2024

... but perhaps configure_dispatch is a little clearer 😅.

Oh, y'all are no fun. 😹

I love the dispatch.meta Idea, though! So would I just add meta as a nested class in Dispatcher? Also, configure_dispatch seems a bit too long, in my opinion; in wondering if there's some way to check if you're defining a function in a class and act accordingly... Maybe inspect.is_method?

@wesselb
Copy link
Member

wesselb commented Apr 27, 2024

@sylvorg, haha feel free to export an alias plume = configure_dispatch too. :P

So would I just add meta as a nested class in Dispatcher?

Maybe use a @property and create the class dynamically:

class Dispatch:
    ...

    @property
    def meta(self):
        class DispatchMeta(type):
            ...
        return meta

How would that look?

Also, configure_dispatch seems a bit too long

Hmm, I do agree, though the user can always do from plum import configure_dispatch as plume or from plum import configure_dispatch as conf or so.

in wondering if there's some way to check if you're defining a function in a class and act accordingly... Maybe inspect.is_method?

Ah, this is clever! How about we get rid of configure_dispatch/plume and just do this:

from plum import Dispatcher, configure_dispatch

dispatch = Dispatcher()


class MyClass(metaclass=dispatch.meta):
    @dispatch(precedence=1)
    def method(self, x):
        return x

Then, in the metaclass, we can check whether the function is already dispatched (in that case it's an instance of plum.function.Function).

@sylvorg
Copy link
Author

sylvorg commented Apr 27, 2024

Got it on the class creation!

When checking a function in the metaclass, then, should we just ignore the already dispatched methods, since they've already been added to the specified dispatcher?

@sylvorg
Copy link
Author

sylvorg commented Apr 27, 2024

Also, I have a very special request to use a different base class for the metaclass if needed; there is a package which provides a metaclass for automatically creating slots from the self variables in the __init__ method, and I'd like to use that if possible; should I just put the __prepare__ class in it's own class externally and use that to create my own version of this, or should there be a way to use different base classes?

@wesselb
Copy link
Member

wesselb commented May 18, 2024

When checking a function in the metaclass, then, should we just ignore the already dispatched methods, since they've already been added to the specified dispatcher?

Yes, that sounds like a good approach. :)

Also, I have a very special request to use a different base class for the metaclass if needed; there is a package which provides a metaclass for automatically creating slots from the self variables in the init method, and I'd like to use that if possible; should I just put the prepare class in it's own class externally and use that to create my own version of this, or should there be a way to use different base classes?

We could do something like this:

from plum import Dispatcher, configure_dispatch

dispatch = Dispatcher()


class MyClass(metaclass=dispatch.meta(ExistingMetaClass)):
    @dispatch(precedence=1)
    def method(self, x):
        return x

where dispatch.meta dynamically creates a metaclass deriving from ExistingMetaClass. plum.parametric currently does something similar.

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

2 participants