Search code examples
pythonfactory-patternmetaclass

python 3.2 plugin factory: instantiation from class/metaclass


I'm riffing from the information here: Metaclass not being called in subclasses

My problem is that I'm unable to create an instance of an object using this class registry. If I use "regular" construction methods, then it seems to instantiate objects correctly; but when I try to use the class object associated with registry, then I get an error that I'm passing an incorrect number of arguments. (Seems to be calling the metaclass new and not my constructor...??)

I'm not clear why it's failing, because I thought I should be able to create an instance from the class object by using "callable" syntax.

Seems I'm getting the metaclass put in the registry and not the class itself? But I don't see an easy way to access the class itself in the new call.

Here is my code example, which fails to instantiate a variable 'd':

registry = [] # list of subclasses

class PluginMetaclass(type):
    def __new__(cls, name, bases, attrs):
        print(cls)
        print(name)
        registry.append((name, cls))
        return super(PluginMetaclass, cls).__new__(cls, name, bases, attrs)

class Plugin(metaclass=PluginMetaclass):
    def __init__(self, stuff):
        self.stuff = stuff

# in your plugin modules
class SpamPlugin(Plugin):
    def __init__(self, stuff):
        self.stuff = stuff

class BaconPlugin(Plugin):
    def __init__(self, stuff):
        self.stuff = stuff

c = SpamPlugin(0)
b = BaconPlugin(0)
mycls = registry[1][1]
d = mycls(0)

Thanks for any help.


Solution

  • I think the issue you're having is that the cls parameter passed to a metaclass constructor is actually a reference to the metaclass and not the class which is being created. Since __new__ is a classmethod of PluginMetaclass, it's associated with that class just like any regular classmethod. You probably want to be registering the newly created class object you're getting from super(PluginMetaclass, cls).__new__(..).

    This modified version worked for me on 3.2:

    class PluginMetaclass(type):
        def __new__(cls, name, bases, attrs):
            print("Called metaclass: %r" % cls)
            print("Creating class with name: %r" % name)
            newclass = super(PluginMetaclass, cls).__new__(cls, name, bases, attrs)
            print("Registering class: %r" % newclass)
            registry.append((name, newclass))
            return newclass
    

    and the print() calls show what's going on behind the scenes:

    >>> registry = []
    >>>
    >>> class Plugin(metaclass=PluginMetaclass):
    ...     def __init__(self, stuff):
    ...         self.stuff = stuff
    ...
    Called metaclass: <class '__main__.PluginMetaclass'>
    Creating class with name: 'Plugin'
    Registering class: <class '__main__.Plugin'>
    >>> class SpamPlugin(Plugin):
    ...     def __init__(self, stuff):
    ...         self.stuff = stuff
    ...
    Called metaclass: <class '__main__.PluginMetaclass'>
    Creating class with name: 'SpamPlugin'
    Registering class: <class '__main__.SpamPlugin'>
    >>> class BaconPlugin(Plugin):
    ...     def __init__(self, stuff):
    ...         self.stuff = stuff
    ...
    Called metaclass: <class '__main__.PluginMetaclass'>
    Creating class with name: 'BaconPlugin'
    Registering class: <class '__main__.BaconPlugin'>
    >>> c = SpamPlugin(0)
    >>> b = BaconPlugin(0)
    >>> mycls = registry[1][1]
    >>> d = mycls(0)
    >>> d
    <__main__.SpamPlugin object at 0x010478D0>
    >>> registry
    [('Plugin', <class '__main__.Plugin'>), 
    ('SpamPlugin', <class '__main__.SpamPlugin'>), 
    ('BaconPlugin', <class '__main__.BaconPlugin'>)]
    

    Edit: @drone115b also solved this by using __init__ instead of __new__ in PluginMetaclass. That's probably the better way to go in most cases.