Olarn's DevBlog
Extending Pymel's node types with 'Virtual Class'
Sorry, failed to load main article image
Bantukul Olarn
    
Nov 29 2017 00:23
  

How would one go about extending pymel class? The first obvious solution was to simply inherit the nodetype. But due how pymel's node creation works, simple inheritance such as the following code below would not behave as expected.

# -*- coding: utf-8 -*-
import pymel.core as pm

class TestExtension(pm.nt.Transform):
    pass

def main():

    # this node creation will fail
    trn = TestExtension(name="testtest")
    print(trn, type(trn))
   
main()

But writing node type extension is still possible using virtual class facility provided by pymel.

A summary of the virtual class mechanism as described by the documentation.

- A class/static method named _isVirtual() is necessary for virtual type determination.

- Optional methods called _preCreate(), _create(), and _postCreate() can be added for finer controls.

- Derived class has to be registered to pymel's node factory in order to use.

_isVirtual ()

Returns a single boolean. 

Called for every registed virtual class type derived from the original pymel's base node, until the first one succeeds. If all test failed, the original pymel type would be used.


Given MObject, and name of the registered virtual class type attempting this test as parameter, determine whether the MObject in question should belong to the virtual type being tested. 


* Since this function is used prior to pymel node creation, only Maya API and maya.cmds could be used here.

* This method is performance critical

_preCreate()

Optional, called before node creation. May return either one or two dict object. The second dict object, if any, will be passed as parameter to _postCreate()



_create()

Optional, not included in this demo.  Overrides the 'default' node creation command. Must return any callback data that _postCreate() and class.__init__ supports.


_postCreate()

Optional, called after node creation. Receives newly create pynode and a dictionary passed from _preCreate(), if exists.

This method are Usually used to modify newly created node in some way that will aid _isVirtual determination when listed from the scene anew.


That's it!

Now let's see it in action. In the sample code below, based on documentation's sample we will be using a custom string attribute to determine the node's virtual type. Newly created node will have this identification attribute added in postCreate(), and isVirtual() would search for the presence and content of this attribute to determine it's virtual type.

Minimal pymel virtual class that supports inheritance.
# -*- coding: utf-8 -*-
from __future__ import print_function, absolute_import, division

import pymel.core as pm
from pymel.internal.factories import virtualClasses
import pprint

# -------------------------------------------------------------------------------

class TypedGrp(pm.nt.Transform):
    @classmethod
    def _isVirtual(cls, obj, name):
        # obj is either an MObject or an MDagPath, depending on whether this class is a subclass of DependNode or DagNode, respectively.
        # we use MFnDependencyNode below because it works with either and we only need to test attribute existence.
        fn = pm.api.MFnDependencyNode(obj)
        try:
            # NOTE: MFnDependencyNode.hasAttribute fails if the attribute does not exist, so we have to try/except it.
            # the _jointClassID is stored on subclass of CustomJointBase
            test_result = fn.hasAttribute(cls._gen_typeid(cls._typeID))
            print("is virtual class", obj, name, test_result)
            # first call:
            # is virtual class <maya.OpenMaya.MObject; proxy of <Swig Object of type 'MObject *' at 0x0000020E12093C00> > samplegroup False
            # seocnd call:
            # is virtual class <maya.OpenMaya.MObject; proxy of <Swig Object of type 'MObject *' at 0x0000020E11F76690> > None True
            return test_result
        except:
            pass

        print("is not virtual class", obj, name)
        return False

    @classmethod
    def _preCreateVirtual(cls, **kwargs):
        print("precreatevirtual")
        postKwargs = {"a_key": "some_value"}
        return kwargs, postKwargs

    @classmethod
    def _postCreateVirtual(cls, newNode, **kwargs):
        print("postcreatevirtual")
        print("argument recieved in post create virtual", newNode, kwargs)
        # argument recieved in post create virtual samplegroup {'a_key': 'some_value'}
        # add the identifying attribute. the attribute name will be set on subclasses of this class
        newNode.addAttr(cls._gen_typeid(cls._typeID), dataType="string")

    @classmethod
    def _gen_typeid(cls, name):
        return u"__vtypeid__{0}".format(name)


class A_GRP(TypedGrp):
    _typeID = 'TYPE_A'


class B_GRP(TypedGrp):
    _typeID = 'TYPE_B'


def main():
    try:
        print("registering virtual class")
        virtualClasses.register(A_GRP, nameRequired=False)

        a_grp1 = A_GRP(name="samplegroup")

        print("newly created nodes, ", a_grp1)
    finally:
        print("unregistering virtual class")
        virtualClasses.unregister(A_GRP)

main()
Total replacement example

In the next example, we will simply make _isVirtual() test return True every time to globally replace a type with the virtual one.

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

import pymel.core as pm
from pymel.internal.factories import virtualClasses
import pprint


class GlobalPymelTransformClassReplacement(pm.nt.Transform):
    @classmethod
    def _isVirtual(cls, obj, name):
        # virtual class determination always return true = this class replaces vanilla Transform class
        # in all cases
        return True

    @classmethod
    def _preCreateVirtual(cls, **kwargs):
        postKwargs = {}
        return kwargs, postKwargs

    @classmethod
    def _postCreateVirtual(cls, newNode, **kwargs):
        pass


def main():
    try:
        virtualClasses.register(GlobalPymelTransformClassReplacement, nameRequired=False)

        a_grp1 = GlobalPymelTransformClassReplacement(name="samplegroup")
        vanilla_grp = pm.nt.Transform(name="normaltransformnode")

        print("newly created nodes, ", [a_grp1, vanilla_grp])
        print("pm.ls by custom transform type")
        pprint.pprint(pm.ls(type=GlobalPymelTransformClassReplacement))
        print("pm.ls by the original Transform type still returns everything as custom transform type")
        pprint.pprint(pm.ls(type=pm.nt.Transform))

    finally:
        virtualClasses.unregister(GlobalPymelTransformClassReplacement)
More examples.

Here we will see how virtual types works with isinstance() function. As well as an alternative virtual Transform class implemented on node name suffix.

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

import pymel.core as pm
from pymel.internal.factories import virtualClasses
import pprint

class TypedTransformBase(pm.nt.Transform):
    _default_name = "TypedTransform"
    @classmethod
    def _isVirtual(cls, obj, name):
        dag_node = pm.api.MFnDependencyNode(obj)
        try:
            return dag_node.hasAttribute(cls._gen_typeid(cls._typeID))
        except:
            pass
        return False

    @classmethod
    def _preCreateVirtual(cls, **kwargs ):
        # give a default name if no name supplied
        if 'name' not in kwargs and 'n' not in kwargs:
            kwargs['name'] = cls._default_name
        postKwargs = {}
        return kwargs, postKwargs

    @classmethod
    def _postCreateVirtual(cls, newNode, **kwargs ):
        newNode.addAttr( cls._gen_typeid(cls._typeID), dataType = "string")

    @classmethod
    def _gen_typeid(cls, name):
        return u"__vtypeid__{0}".format(name)

class ATransform(TypedTransformBase):
    _typeID = 'A_SET'

class BTransform(TypedTransformBase):
    _typeID = 'B_SET'

class PrefixTransformBase(pm.nt.Transform):
    _prefix = "REPLACEME"

    @classmethod
    def _isVirtual(cls, obj, name):
        # assume no namespace and no '|' character!
        dag_node = pm.api.MFnDependencyNode(obj)
        return dag_node.name().startswith(cls._prefix)

    @classmethod
    def _postCreateVirtual(cls, newNode, **kwargs ):
        # assumes no namespace and no '|' character!
        newNode.rename(cls._prefix+newNode.name())

class APrefixTrn(PrefixTransformBase):
    _prefix = "A_TRN_"

class BPrefixTrn(PrefixTransformBase):
    _prefix = "B_TRN_"

def main():
    try:

        virtualClasses.register(ATransform, nameRequired=False)
        virtualClasses.register(BTransform, nameRequired=False)
        virtualClasses.register(APrefixTrn, nameRequired=False)
        virtualClasses.register(BPrefixTrn, nameRequired=False)


        # attribute based virtual class example
        a_grp1 = ATransform()
        a_grp2 = ATransform()
        b_grp = BTransform()
        print("is a_grp1 an instace of ATransform?", isinstance(a_grp1, ATransform))
        print("is a_grp2 also an instance of ATransform?", isinstance(a_grp2, ATransform))
        print("is a_grp1 a derived class instance of TypedTransformBase?",
              issubclass(type(a_grp1), TypedTransformBase))
        print("Is b_grp an isinstance of ATransform?", isinstance(b_grp, ATransform))
        print("Is b_grp BTransform isinstance?", isinstance(b_grp, BTransform))

        # name prefix based virtual class example
        c_grp1 = APrefixTrn(name="samplename")
        c_grp2 = BPrefixTrn(name="anothersamplename")



        print("newly created prefix transform: ", c_grp1, c_grp2)
        print(type(n) for n in [c_grp1,c_grp2])

        # Manually naming a normal Transform object so it would be recognized as APrefixTrn
        c_grp3 = pm.nt.Transform()
        print("vanilla transform type.., ", c_grp3, type(c_grp3))
        c_grp3.rename("B_TRN_manuallyrenamed")
        # Renaming c_grp3 to BPrefixTrn type manually does not updates the pymel wrapper type
        print("renamed c_grp3:", c_grp3, type(c_grp3))
        # But would show up correctly if it were listed again (pymel wrapper has been recreated)
        c_grp3 = pm.ls(c_grp3)[0]
        print("re-listed c_grp3, ", c_grp3, type(c_grp3))

        print("listing all transform based classes")
        pprint.pprint(pm.ls(type=pm.nt.Transform))
        '''
        output:

        [nt.APrefixTrn(u'A_TRN_samplename'),
         nt.BPrefixTrn(u'B_TRN_anothersamplename'),
         nt.BPrefixTrn(u'B_TRN_manuallyrenamed'),
         nt.ATransform(u'TypedTransform'),
         nt.ATransform(u'TypedTransform1'),
         nt.BTransform(u'TypedTransform2'),
         nt.Transform(u'front'),
         nt.Transform(u'persp'),
         nt.Transform(u'side'),
         nt.Transform(u'top')]
        '''

    finally:
        virtualClasses.unregister(ATransform)
        virtualClasses.unregister(BTransform)
        virtualClasses.unregister(APrefixTrn)
        virtualClasses.unregister(BPrefixTrn)

Tags: