Search code examples
python-3.xsubclassabstract-base-class

Getting all registered subclasses of an ABCMeta


I have a directory structure similar to the following:

.
├── main.py
├── model.py
└── models
    ├── __init__.py
    ├── model_a.py
    └── model_b.py

model.py contains an Abstract Base Class:

from abc import ABCMeta, abstractmethod

class Base(metaclass=ABCMeta):

    @abstractmethod
    def run(self):
        pass

in the modelsfolder are two implementations of this base class, model_a.py and model_b.py, who register themselves to the main Baseclass. model_a.py looks like this:

from model import Base

class ModelA(Base):

    def run(self):
        return "a"

ModelA.register(Base)

assert issubclass(ModelA, Base)

and model_b.pyis similar.

Now, what I am trying to do in main.py is to create a dictionary of all the subclasses of Base so that I can select one (via the GUI of my program) and run it:

from model import Base

subclasses = Base.__subclasses__()

dct = {cls.__name__: cls for cls in subclasses}

klass = dct['ModelA']
klass.run()

But I can't get it to work. I get RuntimeError: Refusing to create an inheritance cyclewhen I try to execute one of the derived classes and the dictionary in main.py is empty.


Solution

  • I realise this is rather late, but in case it's helpful to anyone else who stumbles upon this...

    You've got a few problems here:

    1. Your classes are the wrong way round in that register call; it would only make sense in this context to call Base.register(ModelA) (not the other way round) in order to register ModelA as a "virtual subclass" of Base.

    2. Calling ModelA.register(Base) is trying to register Base as a virtual subclass of ModelA, but ModelA is already an actual subclass of Base, - which is why you're getting an inheritance cycle. You can't have classes X and Y inheriting from each other.

    3. However, as ModelA is explicitly a subclass of Base, you don't need to call register at all. You want either:

      class ModelA(Base):
         ...
      

      with no register call (here ModelA is an actual subclass of Base), or:

      class ModelA:
          ...
      
      Base.register(ModelA)
      

      (here ModelA is a standalone class, outside Base's inheritance hierarchy, but it is registered as a virtual subclass). Either/or - not both.

      In either case, issubclass(ModelA, Base) would be True.

    4. __subclasses__() doesn't pick up virtual subclasses, only actual ones - so if you want to use that, you should forget about register() and just make ModelA a real subclass of Base (the first option above).

      (This is, to my mind, a wart with the whole ABC/register mechanism: issubclass() might be True but __subclasses__() doesn't pick it up - nasty.)

    5. If you don't import the model containing ModelA at some point in your execution, it's never set up so ModelA won't show up in Base.__subclassess__() anyway. This is probably why the dictionary in main.py is empty.

      A fix would be to add a line to main.py saying import models, and have models/__init__.py import model_a and model_b. Then when main runs, it imports models, which in turn imports model_a and model_a, executing the definitions of ModelA and ModelB and adding them to Base's class hierarchy.

    6. On your final line, you don't instantiate an instance of whatever class klass is pointing at; the line should be:

      klass().run()