4

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.

M4a1x
  • 114
  • 1
  • 8
  • 1
    Looks like a known bug: https://bugs.python.org/issue35815 – Patrick Haugh May 29 '19 at 18:03
  • Nice, that indeed is exactly the problem. Thanks! As a workaround I just check if `cls` is abstract with `inspect.isabstract(cls)` after an `import inspect` and then throwing a `TypeError` – M4a1x Jun 05 '19 at 20:43

1 Answers1

0

If you just want to force subclasses to implement a method, @abstractmethod is enough. You don't need init_subclass. Your code is correct, however, you have not instantiated Render anywhere. The abstract method will give you an error when creating objects, not classes. If you write Render(), you will actually get an error

Can't instantiate abstract class Render with abstract methods do_something

blue_note
  • 27,712
  • 9
  • 72
  • 90
  • The goal is to 1) register instances of the plugins in the `Plugin` class and 2) force them to have a specific method. The line `class__.plugins.append(cls())` does instantiate though, or did I miss something? – M4a1x May 29 '19 at 11:20
  • (2) is already done with `abstractmethod`. (1) cannot be done by `init_subclass`, because it refers to the class, not its instances. – blue_note May 29 '19 at 11:24
  • I'm not sure I understand what you mean by (1) cannot be done, since I'm doing exactly that in the 'original attempt' which works. The problem is that the instantiation doesn't raise any errors if the subclass does not implement the abstract method. – M4a1x May 29 '19 at 11:52
  • 1
    `Render()` does raise an error, `cls()` inside `__init_sublclass__()` does not. Which is exactly my question. It does raise an error in a normal class function: ```@classmethod def subclass_init(cls): return cls() ``` – M4a1x May 29 '19 at 13:18