I'm trying to write a simple plugin system.
a) For this I found the following code using the 3.6 magic function __init_subclass__
https://stackoverflow.com/a/41924331/1506569 which is adapted from PEP 487, that registers every class on definition in the parent class in the plugins
list.
b) I would now like to "force" the writer of the plugin to implement a specific method, that will be called by my program. Ideally, that would warn the user on import, that his plugin won't work but would not prevent the program from running. For this, I tried to add ABC as the parent class to the Plugin and adding an @abstractmethod
.
My original attempt with ABC and __init_subclass__
:
from abc import ABC, abstractmethod
class Plugin(ABC):
plugins = []
@classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
try:
__class__.plugins.append(cls()) # Does not raise exception
except Exception as e:
print("Warning: Your plugin might not work, missing abstract method")
@abstractmethod
def do_something(self):
...
class Render(Plugin):
def __init__(self):
print("Render init")
# does not implement 'do_something'
for plugin in Plugin.plugins:
plugin.do_something() # Calls 'do_something' on Render, without raising an exception
The problem here is, that the instantiation __class__.plugins.append(cls())
does not raise an exception, instead the instantiated Render
object is added to the plugins
list.
Calling the function Render.do_something
(the loop at the bottom) does not raise an exception either. It does nothing.
A solution I found is described here: https://stackoverflow.com/a/54545842/1506569 which is simply comparing if the function objects on base class and child class are different (by using class and cls). This makes inheriting from ABC unnecessary though and seems more complicated than need be.
The second attempt, without ABC:
class Plugin():
plugins = []
@classmethod
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
if getattr(cls, 'do_something') is getattr(__class__, 'do_something'):
print("Warning: Your plugin might not work, missing 'do_something'")
else:
__class__.plugins.append(cls())
def do_something(self):
...
class Render(Plugin):
def __init__(self):
print("Render init")
for plugin in Plugin.plugins:
plugin.do_something()
This version works, but seems convoluted to me and looks like it might break further down the line?
Is there an approach I missed? Why does the instantiation work? Sorry if I missed a similar question, I tried searching for a long time.