mrv.maya.nt.typ
Covered: 240 lines
Missed: 55 lines
Skipped 196 lines
Percent: 81 %
  2
"""
  3
Houses the MetaClass able to setup new types to work within the system. This can 
  4
be considered the heart of the node wrapping engine, but it plays together with 
  5
the implementation in the `base` module.
  6
"""
  7
__docformat__ = "restructuredtext"
  9
from mrv.maya.util import MetaClassCreator
 10
from mrv.maya.util import MEnumeration
 11
import mrv.maya as mrvmaya
 12
import mrv.maya.mdb as mdb
 13
from mrv.util import uncapitalize, capitalize
 16
import maya.OpenMaya as api
 18
from new import instancemethod
 19
import logging
 20
log = logging.getLogger("mrv.maya.nt.typ")
 22
__all__ = ("MetaClassCreatorNodes", )
 25
_nodesdict = None					# to be set during initialization
 26
nodeTypeTree = None
 27
nodeTypeToMfnClsMap = dict()		# allows to see the most specialized compatible mfn cls for a given node type
 31
targetModule = None				# must be set in intialization to tell this class where to put newly created classes
 32
mfnclsattr = '_mfncls'
 33
mfndbattr = '_mfndb'
 34
apiobjattr = '_apiobj'
 35
getattrorigname = '__getattr_orig'
 36
codegen = None		# python code generator, to be set during initialization
 41
class MetaClassCreatorNodes( MetaClassCreator ):
 42
	"""Builds the base hierarchy for the given classname based on our typetree
 43
	:todo: build classes with slots only as members are pretermined"""
 45
	@classmethod
 46
	def _fetchMfnDB( cls, newcls, mfncls, **kwargs ):
 47
		"""Return the mfndb for the given mfncls as existing on newcls. 
 48
		If it does not yet exist, it will be created and attached first
 50
		:param kwargs: passed to MMemberMap initializer"""
 51
		try:
 52
			return newcls.__dict__[ mfndbattr ]
 53
		except KeyError:
 54
			mfndbpath = mdb.mfnDBPath(mfncls.__name__)
 55
			try:
 56
				mfndb = mdb.MMemberMap(mfndbpath, **kwargs)
 57
			except IOError:
 58
				print IOError("Could not create MFnDB for file at %s" % mfndbpath)
 59
				raise
 61
			type.__setattr__( newcls, mfndbattr, mfndb )
 62
			return mfndb
 65
	@classmethod
 66
	def _wrapStaticMembers( cls, newcls, mfncls ):
 67
		"""Find static mfnmethods - if these are available, initialize the 
 68
		mfn database for the given function set ( ``mfncls`` ) and create properly 
 69
		wrapped methods. 
 70
		Additionally check for enumerations, and generate the respective enumeration
 71
		instances
 72
		:note: As all types are initialized on startup, the staticmethods check 
 73
			will load in quite a few function sets databases as many will have static 
 74
			methods. There is no real way around it, but one could introduce 'packs'
 75
			to bundle these together and load them only once. Probably the performance
 76
			hit is not noticeable, but lets just say that I am aware of it
 77
		:note: Currently method aliases are not implemented for statics !"""
 78
		fstatic, finst = mdb.extractMFnFunctions(mfncls)
 79
		hasEnum = mdb.hasMEnumeration(mfncls)
 80
		if not fstatic and not hasEnum:
 81
			return
 83
		mfndb = cls._fetchMfnDB(newcls, mfncls, parse_enums=hasEnum)
 84
		if fstatic:
 85
			mfnname = mfncls.__name__
 86
			for fs in fstatic:
 87
				fn = fs.__name__
 88
				if fn.startswith(mfnname):
 89
					fn = fn[len(mfnname)+1:]	# cut MFnName_methodName
 92
				static_function = cls._wrapMfnFunc(newcls, mfncls, fn, mfndb)
 93
				type.__setattr__(newcls, fn, staticmethod(static_function))
 98
		if hasEnum:
 99
			for ed in mfndb.enums:
100
				type.__setattr__(newcls, ed.name, MEnumeration.create(ed, mfncls))
105
	@classmethod
106
	def _wrapMfnFunc( cls, newcls, mfncls, funcname, mfndb, addFlags=0 ):
107
		"""Create a function that makes a Node natively use its associated Maya
108
		function set on calls.
110
		The created function will use the api object of the instance of the call to initialize
111
		a function set of type mfncls and execute the function in question.
113
		The method mutation database allows to adjust the way a method is being wrapped
115
		:param mfncls: Maya function set class from which to take the functions
116
		:param funcname: name of the function set function to be wrapped.
117
		:param mfndb: `mdb.MMemberMap` 
118
		:param addFlags: If set, these flags define how the method will be generated.
119
		:raise KeyError: if the given function does not exist in mfncls
120
		:note: if the called function starts with _api_*, a special accellerated method
121
			will be returned and created allowing direct access to the mfn instance method.
122
			This is unsafe if the same api object is being renamed. Also it will only be faster if
123
			the same method is actually called multiple times. It can be great for speed sensitive code
124
			where where the same method(s) are called repeatedly on the same set of objects
125
		:return:  wrapped function or None if it was deleted"""
126
		flags = mfndb.flags|addFlags
127
		funcname_orig = funcname	# store the original for later use
130
		if funcname.startswith( "_api_" ):
131
			flags |= mdb.PythonMFnCodeGenerator.kDirectCall
132
			funcname = funcname[ len( "_api_" ) : ]
134
		mfnfuncname = funcname		# method could be remapped - if so we need to lookup the real name
137
		method_descriptor = None
139
		try:
140
			mfnfuncname, method_descriptor = mfndb.methodByName( funcname )
142
			if method_descriptor.flag == mdb.MMemberMap.kDelete:
143
				return None
144
		except KeyError:
145
			pass # could just be working
148
		if method_descriptor is None:
149
			method_descriptor = mdb.MMethodDescriptor()
159
		mfnfunc = mfncls.__dict__[ mfnfuncname ]			# will just raise on error
160
		if isinstance(mfnfunc, staticmethod):
161
			flags |= mdb.PythonMFnCodeGenerator.kIsStatic
164
			mfnfunc = type.__getattribute__(mfncls, mfnfuncname)
168
		if api.MFnDagNode in mfncls.mro():
169
			flags |= mdb.PythonMFnCodeGenerator.kIsDagNode
170
		if api.MObject in newcls.mro():
171
			flags |= mdb.PythonMFnCodeGenerator.kIsMObject
175
		newfunc = codegen.generateMFnClsMethodWrapperMethod(funcname_orig, funcname, mfncls, mfnfunc, method_descriptor, flags)
177
		if not flags & mdb.PythonMFnCodeGenerator.kIsStatic: 
178
			newfunc.__name__ = funcname			# rename the method
180
		return newfunc
183
	@classmethod
184
	def _wrapLazyGetAttr( thiscls, newcls ):
185
		""" Attach the lazy getattr wrapper to newcls """
187
		if hasattr( newcls, '__getattr__' ):
188
			getattrorig =  newcls.__dict__.get( '__getattr__', None )
189
			if getattrorig:
190
				getattrorig.__name__ = getattrorigname
191
				setattr( newcls, getattrorigname, getattrorig )
195
		def meta_getattr_lazy( self, attr ):
196
			actualcls = None										# the class finally used to store located functions
201
			newclsfunc = newinstfunc = None							# try to fill these
202
			for basecls in newcls.mro():
203
				if not hasattr( basecls, mfnclsattr ):			# could be object too or user defined cls
204
					continue
206
				mfncls = getattr( basecls, mfnclsattr )
207
				if not mfncls:
208
					continue
211
				try:
212
					mfndb = thiscls._fetchMfnDB(basecls, mfncls)
213
					newclsfunc = thiscls._wrapMfnFunc( newcls, mfncls, attr, mfndb )
214
					if not newclsfunc: # Function %s has been deleted - ignore
215
						continue
216
				except KeyError:  		# function not available in this mfn - ignore
217
					continue
218
				newinstfunc = instancemethod( newclsfunc, self, basecls )	# create the respective instance method !
219
				actualcls = basecls
220
				break					# stop here - we found it
226
			if newclsfunc:
228
				object.__setattr__( self, attr, newinstfunc )
229
				type.__setattr__( actualcls, attr, newclsfunc )		# setattr would do too, but its more dramatic this way :)
230
				return newinstfunc
240
			getattrorigfunc = None
241
			for basecls in newcls.mro():
242
				if hasattr( basecls, getattrorigname ):
243
					getattrorigfunc = getattr( basecls, getattrorigname )
244
					break
248
				if hasattr( basecls, '__getattr__' ):
249
					getattrfunc = getattr( basecls, '__getattr__' )
250
					if getattrfunc.func_name != "meta_getattr_lazy":
251
						getattrorigfunc = getattrfunc
252
						break
256
			if not getattrorigfunc:
257
				raise AttributeError( "Could not find mfn function for attribute '%s'" % attr )
261
			return getattrorigfunc( self, attr )
267
		setattr( newcls, "__getattr__", meta_getattr_lazy )
270
	def __new__( metacls, name, bases, clsdict ):
271
		""" Called to create the class with name """
273
		def func_nameToTree( name ):
277
			name = uncapitalize(name)
278
			if nodeTypeTree.has_node(name):
279
				return name
281
			capname = capitalize(name)
282
			if nodeTypeTree.has_node(capname):
283
				return capname
285
			return name
292
		mfncls = None
293
		needs_static_method_initialization = False
294
		if not clsdict.has_key( mfnclsattr ):
295
			treeNodeTypeName = func_nameToTree( name )
296
			if nodeTypeToMfnClsMap.has_key( treeNodeTypeName ):
297
				mfncls = nodeTypeToMfnClsMap[ treeNodeTypeName ]
298
				clsdict[ mfnclsattr ] = mfncls
301
				needs_static_method_initialization = True
303
		else:
304
			mfncls = clsdict[ mfnclsattr ]
308
		if mfncls:
309
			clsdict[ mfnclsattr ] = mfncls			# we have at least a None mfn
310
		clsdict[ apiobjattr ] = None			# always have an api obj
315
		newcls = super( MetaClassCreatorNodes, metacls ).__new__( nodeTypeTree, targetModule,
316
																metacls, name, bases, clsdict,
317
																nameToTreeFunc = func_nameToTree )
326
		if mfncls:
327
			metacls._wrapLazyGetAttr( newcls )
329
			if needs_static_method_initialization:
330
				metacls._wrapStaticMembers(newcls, mfncls)
334
		return newcls
343
def prefetchMFnMethods():
344
	"""Fetch and install all mfn methods on all types supporting a function set.
345
	This should only be done to help interactive mode, but makes absolutely no 
346
	sense in the default mode of operation when everything is produced on demand.
348
	:note: Attaches docstrings as well
349
	:return: integer representing the number of generated methods"""
350
	log.info("Prefetching all MFnMethods")
352
	num_fetched = 0
353
	for typename, mfncls in nodeTypeToMfnClsMap.iteritems():
354
		try:
355
			nodetype = _nodesdict[capitalize(typename)]
356
		except KeyError:
357
			log.debug("MFn methods for %s exists, but type was not found in nt" % typename)
358
			continue
361
		mfnname = mfncls.__name__
362
		mfndb = MetaClassCreatorNodes._fetchMfnDB(nodetype, mfncls)
363
		fstatic, finst = mdb.extractMFnFunctions(mfncls)
365
		def set_method_if_possible(cls, fn, f):
366
			if not hasattr(cls, fn):
367
				type.__setattr__(cls, fn, f)
371
		for f in finst:
372
			fn = f.__name__
373
			if fn.startswith(mfnname):
374
				fn = fn[len(mfnname)+1:]
377
			fna = fn		# alias for method 
378
			try:
379
				origname, entry = mfndb.methodByName(fn)
380
				fna = entry.newname
381
			except KeyError:
382
				pass
385
			fwrapped = MetaClassCreatorNodes._wrapMfnFunc(nodetype, mfncls, fn, mfndb, mdb.PythonMFnCodeGenerator.kWithDocs)
388
			if fwrapped is None:
389
				continue
391
			set_method_if_possible(nodetype, fn, fwrapped)
392
			if fna != fn:
393
				set_method_if_possible(nodetype, fna, fwrapped)
396
			num_fetched += 1
400
	return num_fetched
408
def _addCustomType( targetmoduledict, parentclsname, newclsname,
409
				   	metaclass=MetaClassCreatorNodes, **kwargs ):
410
	"""Add a custom type to the system such that a node with the given type will
411
	automatically be wrapped with the corresponding class name
413
	:param targetmoduledict: the module's dict to which standin classes are supposed to be added
414
	:param parentclsname: the name of the parent node type - if your new class
415
		has several parents, you have to add the new types beginning at the first exsiting parent
416
		as written in the maya/cache/nodeHierarchy.html file
417
	:param newclsname: the new name of your class - it must exist targetmodule
418
	:param metaclass: meta class object to be called to modify your type upon creation
419
		It will not be called if the class already exist in targetModule. Its recommended to derive it
420
		from the metaclass given as default value.
421
	:raise KeyError: if the parentclsname does not exist"""
423
	parentclsname = uncapitalize( parentclsname )
424
	newclsname = uncapitalize( newclsname )
425
	nodeTypeTree.add_edge( parentclsname, newclsname )
428
	mrvmaya.initWrappers( targetmoduledict, [ newclsname ], metaclass, **kwargs )
431
def _removeCustomType( targetmoduledict, customTypeName ):
432
	"""Remove the given typename from the given target module's dictionary as 
433
	well as from internal caches
435
	:note: does nothing if the type does not exist
436
	:param targetmoduledict: dict of your module to remove the type from
437
	:param customTypeName: name of the type to be removed, its expected
438
		to be capitalized"""
439
	try:
440
		del(targetmoduledict[customTypeName])
441
	except KeyError:
442
		pass
445
	customTypeName = uncapitalize(customTypeName)
446
	if nodeTypeTree.has_node(customTypeName):
447
		nodeTypeTree.remove_node(customTypeName)
450
def _addCustomTypeFromDagtree( targetmoduledict, dagtree, metaclass=MetaClassCreatorNodes,
451
							  	force_creation=False, **kwargs ):
452
	"""As `_addCustomType`, but allows to enter the type relations using a
453
	`mrv.util.DAGTree` instead of individual names. Thus multiple edges can be added at once
455
	:note: special care is being taken to make force_creation work - first all the standind classes
456
		are needed, then we can create them - just iterating the nodes in undefined order will not work
457
		as a parent node might not be created yet
458
	:note: node names in dagtree must be uncapitalized"""
460
	rootnode = dagtree.get_root()
461
	def recurseOutEdges( node ):		# postorder
462
		for child in dagtree.children_iter( node ):
463
			yield (node,child)
464
			for edge in recurseOutEdges( child ):	# step down the hierarchy
465
				yield edge
467
	nodeTypeTree.add_edges_from( recurseOutEdges( rootnode ) )
468
	mrvmaya.initWrappers( targetmoduledict, dagtree.nodes_iter(), metaclass, force_creation = force_creation, **kwargs )
470
def initTypeNameToMfnClsMap( ):
471
	"""Fill the cache map supplying additional information about the MFNClass to use
472
	when creating the classes"""
473
	global nodeTypeToMfnClsMap
474
	nodeTypeToMfnClsMap = mdb.createTypeNameToMfnClsMap()
476
def initNodeHierarchy( ):
477
	"""Initialize the global tree of types, providing a hierarchical relationship between 
478
	the node typename strings"""
479
	global nodeTypeTree
480
	nodeTypeTree = mdb.createDagNodeHierarchy()
482
def initWrappers( targetmoduledict ):
483
	"""Create Standin Classes that will delay the creation of the actual class till
484
	the first instance is requested
486
	:param targetmoduledict: the module's dictionary (globals()) to which to put the wrappers"""
487
	global nodeTypeTree
488
	mrvmaya.initWrappers( targetmoduledict, nodeTypeTree.nodes_iter(), MetaClassCreatorNodes )