samedi 8 janvier 2022

Plugin like extensibility in Python - what is the pattern?

It feels like I am missing the Shibboleth, so I hope the answer to my question below will be: you are looking for the {pattern} name.

I am working on a Python (3.10) project where I want to allow plugin like extensibility that would allow for two way communication between modules in the same project.

Imagine a two way pub/sub pattern without asynchronous messaging. Coming from OOP I would call it a dynamic dispatch and implement it with a little bit of reflection.

Unfortunately for me, it appears that dynamic dispatch has very specific meaning in Python and it relates to implementing polymorphic like behaviour.

A very simple example of what I want to achieve is this. Imagine there are three modules/classes/files in a project.

# somewhere in module 1
def option_one(self):
  return 'first option'
# somewhere in module 2
def option_two(self):
  return 'second option'
# somewhere in module 3
def options(self):
  # implementation of some patter
  # name I cannot figure out

# eventually execute it from here
print(options())

The result should be that the print out is:

[ 'first option', 'second option' ]

The important part is, that there should be no need to hard code in module 3 anything that would imply ahead of time knowledge of the specific implementations.

So far I have been experimenting with the Registry pattern and subclassing, and it works with single method, but I need to be able to define multiple methods some of which may, or may not be implemented.

My current attempt looks like so, but it does feel like I am missing something.

class PluginBase:
    instances = []

    # modified registry pattern that instantiates subclasses
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.instances.append(cls())

    # the decorator to turn functions into 'dispatch'
    def query(func: callable) -> callable:
        def wrapper():            
            results = []
            for i in PluginBase.instances:
                fn = func.__name__
                if getattr(i, fn, None) \
                    and not getattr(PluginBase, fn).__qualname__==getattr(i, fn).__qualname__:
                    try:
                        results += PluginBase.always_array(getattr(i, fn)())
                    except Exception as err:
                        print(err)
                        pass
            return results
        return wrapper

    @staticmethod
    def always_array(value: any) -> list:
        if type(value) == list:  
            return value
        else: 
            return [value]

    @query 
    def options() -> list: 
        return []
class First(PluginBase):
    def options(self) -> any:
        return 'from first'
class Second(PluginBase):
    def options(self) -> any:
        return ['from second']


# This produces the desired output
print(PluginBase.options())

What am I missing? What is the Shibboleth, the word that unlocks it all?

Aucun commentaire:

Enregistrer un commentaire