First, lets fix a trivial mistake in the code, and then dig into the "real problem": the bases
parameter needs to be a tuple. when you do bases=(option)
the right hand side is not a tuple - it is merely a parenthesized expression that will be resolved and passed on as the non-tuple option
.
Change that to bases=(option,)
whenever you need to create a tuple for the bases.
The second mistake is more conceptual, and is probably why you didn't get it to work across varios attempts: the __call__
method of the metaclass is not something one usually fiddles with. To keep a long history short, the __call__
method of a __class__
is what is called to coordinate the calling of the __new__
and __init__
methods of instances of that class - that is made by Python automatically, and it is the __call__
from type
that has this mechanism. When you transpose that to your metaclass you might realise that the __call__
method of your metaclass is not used when the __new__
and __init__
methods of your metaclass itself are about to be called (when a class is defined). In other words - the __call__
that is used is on the "meta-meta" class (which is again, type).
The __call__
method you wrote will instead be used by the instances of your custom classes when they are created (and this is what you intended), and will have no effect on class creation as it won't invoke the metaclass' __new__
- just the class __new__
itself. (and this is not what you intended).
So, what you need is, from inside __call__
not to call super().__call__
with the same arguments you received: that will pass cls
to type
's call, and the bases for cls
are baked in when the metaclass __new__
was run - it just happens when the class body itself is declared.
You must have, in this __call__
dynamically create a new class, or use one of a pre-filled in table, and them pass that dynamically created class to the type.__call__
.
But - in the end of the day, one can perceive that all this can be done with a factory function, so there is no need to create this super-complicated meta-class mechanism for that - and other Python tools such as linters and static analysers (as embbeded in an IDE you or your colleagues may be using) might work better with that.
Solution using factory function:
def factory(cls, *args, options=None, **kwargs):
if options == 'thingy':
cls = type(cls.__name__, (option1, ), cls.__dict__)
elif options = 'other':
...
return cls(*args, **kwargs)
If you don't want to create a new class on every call, but want to share a couple of pre-existing classes with common bases, just create a cache-dictionary, and use the dict's setdefault
method:
class_cache = {}
def factory(cls, *args, options=None, **kwargs):
if options == 'thingy':
cls = class_cache.setdefault((cls.__name__, options),
type(cls.__name__, (option1, ), cls.__dict__))
elif options = 'other':
...
return cls(*args, **kwargs)
(The setdefault method will store the second argument on the dict if the key (name, options) does not exist yet).
using a metaclass:
updated
After breakfast :-) I came up with this:
make your metaclass __new__
inject a __new__
function on the created class itself that will either create a new class, with the desired bases dynamically or used a cached one for the same options. But unlike the other example, use a metaclass to anotate the original parameters to the class creation to create the derived class:
parameter_buffer = {}
derived_classes = {}
class Meta:
def __new__(metacls, name, bases, namespace):
cls = super().__new__(metacls, name, bases, namespace)
parameter_buffer[cls] = (name, bases, namespace)
def __new__(cls, *args, option=None, **kwargs):
if option is None:
return original_new(cls, *args, **kwargs)
name, bases, namespace = parameter_buffer[cls]
if option == 'thingy':
bases = (option1,)
elif option== 'thingy2':
...
if not (cls, bases) in derived_classes:
derived_classes[cls, bases] = type(name, bases, namespace)
return derived_classes[cls, bases](*args, **kwargs)
cls.__new__ = __new__
return cls
To keep the example short, this simply overwrites any explict __new__
method on the class that uses this metaclass. Also, the derived classes created this way are not themselves bearer of the same capability since they are created calling type
and the metaclass is discarded in the process. Both things could be taken care off by writing more careful code but it would become complicated as an example here.