plum icon indicating copy to clipboard operation
plum copied to clipboard

`multimeta`-like metaclass for plum

Open sylvorg opened this issue 4 months ago ā€¢ 12 comments

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!

sylvorg avatar Mar 17 '24 16:03 sylvorg

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.

wesselb avatar Mar 20 '24 12:03 wesselb

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

sylvorg avatar Mar 20 '24 16:03 sylvorg

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.

wesselb avatar Mar 25 '24 19:03 wesselb

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?

sylvorg avatar Mar 25 '24 23:03 sylvorg

@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. :)

wesselb avatar Apr 20 '24 11:04 wesselb

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.

sylvorg avatar Apr 27 '24 03:04 sylvorg

@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 šŸ˜….

wesselb avatar Apr 27 '24 18:04 wesselb

... 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?

sylvorg avatar Apr 27 '24 18:04 sylvorg

@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).

wesselb avatar Apr 27 '24 19:04 wesselb

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 avatar Apr 27 '24 19:04 sylvorg

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?

sylvorg avatar Apr 27 '24 19:04 sylvorg