5

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.

2
  • 1
    Looks like a known bug: bugs.python.org/issue35815 Commented May 29, 2019 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 Commented Jun 5, 2019 at 20:43

1 Answer 1

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

Sign up to request clarification or add additional context in comments.

4 Comments

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?
(2) is already done with abstractmethod. (1) cannot be done by init_subclass, because it refers to the class, not its instances.
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.
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()

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.