Olarn's DevBlog
Remove or replace base python metaclass in derived class by using the "Anti-Metaclass"
Sorry, failed to load main article image
Bantukul Olarn
    
Jun 09 2018 16:40
  

Removing and replace base python metaclass in derived class, implemented as another metaclass, the metaclass removing "anti-metaclass"

Many good article has been written about using python metaclasses. Please refer to them for basic concept of python metaclasses

https://docs.python.org/2/reference/datamodel.html#customizing-class-creation

https://docs.python.org/3.6/reference/datamodel.html#customizing-class-creation

https://python-3-patterns-idioms-test.readthedocs.io/en/latest/Metaprogramming.html

This article will be exclusively using @six.add_metaclass function from six package to provide compatibility across python 2 and 3. (Although you have to replace pymel.utils.Singletron metaclass with something else in 3)

https://pythonhosted.org/six/#syntax-compatibility

* for a statically removing metaclass without using metaclass, see the last section

ASingletronClass class will be the original singletron class with pymel.util.Singletron as it's metaclass (from maya's pymel package).

The goal here is to remove the original singletron behavior from the base class.

NotASingletronReally_Copy class will demonstrate manual copy approach where a derived class does not actually inherits the base class, but it's gut manually transplanted inside metaclass's __new__

NotASingletronReally_ReplaceMeta_Standard and NotASingletronReally_ReplaceMeta class will demonstrate dynamic base class replacement approach. A temporary (permanent actually.., see warnings below) copy of the base class will be created inside __new__. After that the original metaclass will be removed from base and class's dict, and returns the resulting type from invocation of replacement metaclass's __new__.

NotASingletronReally_Static derives from ASingletronClass with it's metaclass removed, NoMoreSingleTron via remove_meta_type.

Notes, warning and limitation:

  • Metaclass argument in this example are passed by class attribute that will be consumed inside the metaclass
  • (replacement) limitation: only one base class supported, only the first base class will be processed.
  • Your IDE's autocomplete/code inspector probably won't like the copy(and also the static) approach.
  • issubclass(), isinstance() subtype check against the immediate base class could fail as a new dynamic base class is created for each metaclass calls. A simple workaround is to implement some caching scheme in the metaclass

# -*- coding: utf-8 -*-
from __future__ import (
    print_function,
    absolute_import,
    division,
)

import pymel.util
import six


@six.add_metaclass(pymel.util.Singleton)
class ASingletronClass(object):

    def __init__(self):
        self.an_attr = 0
        print("init,", self)

    def some_method(self):
        return self.an_attr

    @property
    def some_prop(self):
        return self.an_attr

    @some_prop.setter
    def some_prop(self, val):
        self.an_attr = val


class EchoMeta(type):
    # debug meta
    def __new__(
            metacls,
            classname,
            bases,
            classdict
    ):
        print("echo meta", EchoMeta, metacls, classname, bases, classdict)

        return type.__new__(
            metacls,
            classname,
            bases,
            classdict
        )


META_COPY_BLACKLIST = [
    "__metaclass__",
    "__class__",
    "__dict__",
    "__module__",
    # "__getattribute__",
    # "__setattr__",
    "__hash__",
    # "__format__",
    "__new__",
    # "__repr__",
    # "__reduce__",
    # "__reduce_ex__",
    # "__sizeof__",
    # "__str__",
    # "__subclasshook__",
    # "__weakref__",
]


class TypeCopyMeta(type):
    META_ATTR_NAME = "META_ARGS"
    TARGET_BASE_CLASS = "target_base_cls"

    def __new__(
            metacls,
            classname,
            bases,
            classdict
    ):

        try:
            meta_args = classdict.pop(metacls.META_ATTR_NAME)
            target_base_class = meta_args.pop(metacls.TARGET_BASE_CLASS)
        except KeyError as e:
            raise Exception("Invalid TypeCopyMeta meta parameters {0}".format(e))

        for attr_name in dir(target_base_class):
            if any([attr_name in tn for tn in META_COPY_BLACKLIST]):
                continue

            try:
                classdict[attr_name] = target_base_class.__dict__[attr_name]
                print("attr transferred,", attr_name, target_base_class.__dict__[attr_name])
            except:
                pass
                # print("fail, skip", attr_name)

        new_type = type.__new__(
            metacls,
            classname,
            bases,
            classdict
        )

        return new_type


class ReplaceMeta(type):
    META_ATTR_NAME = "META_ARGS"
    REPLACE_META_CLS = "replace_meta_cls"
    COPIED_BASE_SUFFIX = "_ReplaceMeta"

    def __new__(
            metacls,
            classname,
            bases,
            classdict
    ):
        u"""limitation: one base class only, or only the first base class will be used"""

        try:
            # extract metaclass args from class, if any
            meta_args = classdict.pop(metacls.META_ATTR_NAME)
            replace_meta_cls = meta_args.pop(metacls.REPLACE_META_CLS)
        except KeyError as e:
            replace_meta_cls = type

        base_cls = bases[0]
        new_base_cls_dict = base_cls.__dict__.copy()

        # clear __metaclass__ from base class
        new_base_cls_dict.pop("__metaclass__", None)
        # previous metaclass could have modified the __new__, making metaclass
        # replacement pointless. so it is removed from both bases and class dict
        new_base_cls_dict.pop("__new__", None)
        new_base_cls_dict.pop("__new__", None)
        classdict.pop("__new__", None)

        new_base_type = type.__new__(
            metacls,
            base_cls.__name__ + metacls.COPIED_BASE_SUFFIX,
            base_cls.__bases__,
            new_base_cls_dict,
        )

        # invoke replacement metaclass, if any, or just standard type
        new_type = replace_meta_cls.__new__(
            metacls,
            classname,
            (new_base_type,),
            classdict
        )

        return new_type


@six.add_metaclass(TypeCopyMeta)
class NotASingletronReally_Copy(object):
    META_ARGS = {
        TypeCopyMeta.TARGET_BASE_CLASS: ASingletronClass
    }

    def __init__(self):
        super(NotASingletronReally_Copy, self).__init__()


@six.add_metaclass(ReplaceMeta)
class NotASingletronReally_ReplaceMeta_Standard(ASingletronClass):

    def __init__(self):
        super(NotASingletronReally_ReplaceMeta_Standard, self).__init__()


@six.add_metaclass(ReplaceMeta)
class NotASingletronReally_ReplaceMeta(ASingletronClass):
    META_ARGS = {
        ReplaceMeta.REPLACE_META_CLS: EchoMeta
    }

    def __init__(self):
        super(NotASingletronReally_ReplaceMeta, self).__init__()


def testit(msg, testtype):
    print(msg)
    print("creating not singletron, a, b")
    a = testtype()
    b = testtype()
    print("a.an_attr = 456")
    a.an_attr = 456

    print("a.an_attr")
    print(a.an_attr)
    print("a.some_method")
    print(a.some_method())
    print("a.some_prop")
    print(a.some_prop)

    print("b.an_attr")
    print(b.an_attr)
    print("b.some_method")
    print(b.some_method())
    print("b.some_prop")
    print(b.some_prop)

    print("test prop")
    print("a.some_prop = 777")
    a.some_prop = 777
    print("a.some_prop")
    print(a.some_prop)
    print("b.some_prop")
    print(b.some_prop)


def remove_meta_type(type_):
    new_type_dict = type_.__dict__.copy()
    new_type_dict.pop("__metaclass__", None)
    new_type_dict.pop("__new__", None)
    new_type = type.__new__(
        type,
        type_.__name__ + "_NoMeta",
        type_.__bases__,
        new_type_dict,
    )
    return new_type

NoMoreSingleTron = remove_meta_type(ASingletronClass)

NotASingletronReally_Static = remove_meta_type(NoMoreSingleTron)

testit("------original singletron class", ASingletronClass)
testit("------singletron metaclass removed, copy method", NotASingletronReally_Copy)
testit("------singletron metaclass removed, replace method",
       NotASingletronReally_ReplaceMeta_Standard)
testit("------singletron metaclass removed, replace method, metaclass replaced with "
       "another custom class", NotASingletronReally_ReplaceMeta)
testit("------singletron metaclass removed, static method", NotASingletronReally_Static)

output

attr transferred, __doc__ None
attr transferred, __init__ <function __init__ at 0x000001D5B1A56588>
attr transferred, __weakref__ <attribute '__weakref__' of 'ASingletronClass' objects>
attr transferred, some_method <function some_method at 0x000001D5B1A565F8>
attr transferred, some_prop <property object at 0x000001D5B1A49B38>
echo meta <class '__main__.EchoMeta'> <class '__main__.ReplaceMeta'> NotASingletronReally_ReplaceMeta (<class '__main__.ASingletronClass_ReplaceMeta'>,) {'__module__': '__main__', '__init__': <function __init__ at 0x000001D5B1A56A58>, '__doc__': None}
------original singletron class
creating not singletron, a, b
init, <__main__.ASingletronClass object at 0x000001D5B20AD1D0>
init, <__main__.ASingletronClass object at 0x000001D5B20AD1D0>
a.an_attr = 456
a.an_attr
456
a.some_method
456
a.some_prop
456
b.an_attr
456
b.some_method
456
b.some_prop
456
test prop
a.some_prop = 777
a.some_prop
777
b.some_prop
777
------singletron metaclass removed, copy method
creating not singletron, a, b
init, <__main__.NotASingletronReally_Copy object at 0x000001D5B20ADB70>
init, <__main__.NotASingletronReally_Copy object at 0x000001D5B205E6D8>
a.an_attr = 456
a.an_attr
456
a.some_method
456
a.some_prop
456
b.an_attr
0
b.some_method
0
b.some_prop
0
test prop
a.some_prop = 777
a.some_prop
777
b.some_prop
0
------singletron metaclass removed, replace method
creating not singletron, a, b
init, <__main__.NotASingletronReally_ReplaceMeta_Standard object at 0x000001D5B205E6D8>
init, <__main__.NotASingletronReally_ReplaceMeta_Standard object at 0x000001D5B20ADB70>
a.an_attr = 456
a.an_attr
456
a.some_method
456
a.some_prop
456
b.an_attr
0
b.some_method
0
b.some_prop
0
test prop
a.some_prop = 777
a.some_prop
777
b.some_prop
0
------singletron metaclass removed, replace method, metaclass replaced with another custom class
creating not singletron, a, b
init, <__main__.NotASingletronReally_ReplaceMeta object at 0x000001D5B20ADB70>
init, <__main__.NotASingletronReally_ReplaceMeta object at 0x000001D5B205E6D8>
a.an_attr = 456
a.an_attr
456
a.some_method
456
a.some_prop
456
b.an_attr
0
b.some_method
0
b.some_prop
0
test prop
a.some_prop = 777
a.some_prop
777
b.some_prop
0
------singletron metaclass removed, static method
creating not singletron, a, b
init, <__main__.ASingletronClass_NoMeta_NoMeta object at 0x000001D5B205E6D8>
init, <__main__.ASingletronClass_NoMeta_NoMeta object at 0x000001D5B20ADB70>
a.an_attr = 456
a.an_attr
456
a.some_method
456
a.some_prop
456
b.an_attr
0
b.some_method
0
b.some_prop
0
test prop
a.some_prop = 777
a.some_prop
777
b.some_prop
0

Tags: