mrv.maya.ns
Covered: 313 lines
Missed: 27 lines
Skipped 149 lines
Percent: 92 %
  2
"""
  3
Allows convenient access and handling of namespaces in an object oriented manner
  4
"""
  5
__docformat__ = "restructuredtext"
  7
import undo
  8
from mrv.maya.util import noneToList
  9
from mrv.interface import iDagItem
 10
from mrv.util import CallOnDeletion
 11
import maya.cmds as cmds
 12
import maya.OpenMaya as api
 13
import re
 15
__all__ = ("Namespace", "createNamespace", "currentNamespace", "findUniqueNamespace", 
 16
           "existsNamespace", "RootNamespace")
 19
def _isRootOf( root, other ):
 20
	""":return: True if other which may be a string, is rooted at 'root
 21
	:param other: '' = root namespace'
 22
		hello:world => :hello:world
 23
	:param root: may be namespace or string. It must not have a ':' in front, 
 24
		hence it must be a relative naespace, and must end with a ':'.
 25
	:note: the arguments are very specific, but this allows the method 
 26
		to be faster than usual"""
 27
	return (other+':').startswith(root)
 30
class Namespace( unicode, iDagItem ):
 31
	""" Represents a Maya namespace
 32
	Namespaces follow the given nameing conventions:
 34
	 - Paths starting with a column are absolute
 36
	  - :absolute:path
 38
	 - Path separator is ':'
 39
	 """
 40
	re_find_duplicate_sep = re.compile( ":{2,}" )
 41
	_sep = ':'
 42
	rootpath = ':'
 43
	_defaultns = [ 'UI','shared' ]			# default namespaces that we want to ignore in our listings
 44
	defaultIncrFunc = lambda b,i: "%s%02i" % ( b,i )
 47
	__slots__ = tuple()
 51
	def __new__( cls, namespacepath=rootpath, absolute = True ):
 52
		"""Initialize the namespace with the given namespace path
 54
		:param namespacepath: the namespace to wrap - it should be absolut to assure
 55
			relative namespaces will not be interpreted in an unforseen manner ( as they
 56
			are relative to the currently set namespace
 58
			Set it ":" ( or "" ) to describe the root namespace
 59
		:param absolute: if True, incoming namespace names will be made absolute if not yet the case
 60
		:note: the namespace does not need to exist, but many methods will not work if so.
 61
			NamespaceObjects returned by methods of this class are garantueed to exist"""
 63
		if namespacepath != cls.rootpath:
 64
			if absolute:
 65
				if not namespacepath.startswith( ":" ):		# do not force absolute namespace !
 66
					namespacepath = ":" + namespacepath
 68
			if len( namespacepath ) > 1 and namespacepath.endswith( ":" ):
 69
				namespacepath = namespacepath[:-1]
 71
		return unicode.__new__( cls, namespacepath )
 73
	def __add__( self, other ):
 74
		"""Properly catenate namespace objects - other must be relative namespace or
 75
		object name ( with or without namespace )
 77
		:return: new string object """
 78
		inbetween = self._sep
 79
		if self.endswith( self._sep ) or other.startswith( self._sep ):
 80
			inbetween = ''
 82
		return "%s%s%s" % ( self, inbetween, other )
 84
	def __repr__( self ):
 85
		return "Namespace('%s')" % str( self )
 89
	@classmethod
 90
	@undo.undoable
 91
	def create( cls, namespaceName ):
 92
		"""Create a new namespace
 94
		:param namespaceName: the name of the namespace, absolute or relative -
 95
			it may contain subspaces too, i.e. :foo:bar.
 96
			fred:subfred is a relative namespace, created in the currently active namespace
 97
		:note: if the target namespace already exists, it will be returned
 98
		:return: the create Namespace object"""
 99
		newns = cls( namespaceName )
101
		if newns.exists():		 # skip work
102
			return newns
104
		cleanup = CallOnDeletion( None )
105
		if newns.isAbsolute():	# assure root is current if we are having an absolute name
106
			previousns = Namespace.current()
107
			cls( Namespace.rootpath ).setCurrent( )
108
			cleanup.callableobj = lambda : previousns.setCurrent()
111
		tokens = newns.split( newns._sep )
112
		for i,token in enumerate( tokens ):		# skip the root namespac
113
			base = cls( ':'.join( tokens[:i+1] ) )
114
			if base.exists( ):
115
				continue
120
			op = undo.GenericOperation( )
121
			op.setDoitCmd( cmds.namespace, p=base.parent() , add=base.basename() )
122
			op.setUndoitCmd(cmds.namespace, rm=base )
123
			op.doIt( )
126
		return newns
128
	def rename( self, newName ):
129
		"""Rename this namespace to newName - the original namespace will cease to exist
131
		:note: if the namespace already exists, the existing one will be returned with
132
			all objects from this one added accordingly
133
		:param newName: the absolute name of the new namespace
134
		:return: Namespace with the new name
135
		:todo: Implement undo !"""
136
		newnamespace = self.__class__( newName )
140
		def moveChildren( curparent, newname ):
141
			for child in curparent.children( ):
142
				moveChildren( child, newname + child.basename( ) )
144
			curparent.delete( move_to_namespace = newname, autocreate=True )
146
		moveChildren( self, newnamespace )
147
		return newnamespace
149
	def moveNodes( self, targetNamespace, force = True, autocreate=True ):
150
		"""Move objects from this to the targetNamespace
152
		:param force: if True, namespace clashes will be resolved by renaming, if false
153
			possible clashes would result in an error
154
		:param autocreate: if True, targetNamespace will be created if it does not exist yet
155
		:todo: Implement undo !"""
156
		targetNamespace = self.__class__( targetNamespace )
157
		if autocreate and not targetNamespace.exists( ):
158
			targetNamespace = Namespace.create( targetNamespace )
160
		cmds.namespace( mv=( self, targetNamespace ), force = force )
162
	def delete( self, move_to_namespace = rootpath, autocreate=True ):
163
		"""Delete this namespace and move it's obejcts to the given move_to_namespace
165
		:param move_to_namespace: if None, the namespace to be deleted must be empty
166
			If Namespace, objects in this namespace will be moved there prior to namespace deletion
167
			move_to_namespace must exist
168
		:param autocreate: if True, move_to_namespace will be created if it does not exist yet
169
		:note: can handle sub-namespaces properly
170
		:raise RuntimeError:
171
		:todo: Implement undo !"""
172
		if self == self.rootpath:
173
			raise ValueError( "Cannot delete root namespace" )
175
		if not self.exists():					# its already gone - all fine
176
			return
179
		if move_to_namespace:
180
			move_to_namespace = self.__class__( move_to_namespace )
183
		previousns = Namespace.current( )
184
		cleanup = CallOnDeletion( None )
185
		if previousns != self:		# cannot reset future deleted namespace
186
			cleanup.callableobj = lambda : previousns.setCurrent()
190
		for childns in self.children( ):
191
			childns.delete( move_to_namespace = move_to_namespace )
194
		self.setCurrent( )
196
		if move_to_namespace:
197
			self.moveNodes( move_to_namespace, autocreate=autocreate )
200
		cmds.namespace( rm=self )
203
	@undo.undoable
204
	def setCurrent( self ):
205
		"""Set this namespace to be the current one - new objects will be put in it
206
		by default
208
		:return: self"""
210
		melop = undo.GenericOperation( )
211
		melop.setDoitCmd( cmds.namespace, set = self )
212
		melop.setUndoitCmd( cmds.namespace, set = Namespace.current() )
213
		melop.doIt()
215
		return self
219
	def parent( self ):
220
		""":return: parent namespace of this instance"""
221
		if self == self.rootpath:
222
			return None
224
		parent = iDagItem.parent( self )	# considers children like ":bar" being a root
225
		if parent == None:	# we are just child of the root namespcae
226
			parent = self.rootpath
227
		return self.__class__( parent )
229
	def children( self, predicate = lambda x: True ):
230
		""":return: list of child namespaces
231
		:param predicate: return True to include x in result"""
232
		lastcurrent = self.current()
233
		self.setCurrent( )
234
		out = []
235
		for ns in noneToList( cmds.namespaceInfo( lon=1 ) ):		# returns root-relative names !
236
			if ns in self._defaultns or not predicate( ns ):
237
				continue
238
			out.append( self.__class__( ns ) )
240
		lastcurrent.setCurrent( )
242
		return out
246
	@classmethod
247
	def current( cls ):
248
		""":return: the currently set absolute namespace """
250
		nsname = cmds.namespaceInfo( cur = 1 )
251
		if not nsname.startswith( ':' ):		# assure we return an absoslute namespace
252
			nsname = ":" + nsname
253
		return cls( nsname )
255
	@classmethod
256
	def findUnique( cls, basename, incrementFunc = defaultIncrFunc ):
257
		"""Find a unique namespace based on basename which does not yet exist
258
		in the scene and can be created.
260
		:param basename: basename of the namespace, like ":mynamespace" or "mynamespace:subspace"
261
		:param incrementFunc: func( basename, index ), returns a unique name generated
262
			from the basename and the index representing the current iteration
263
		:return: unique namespace that is guaranteed not to exist below the current
264
			namespace"""
265
		i = 0
266
		while True:
267
			testns = cls( incrementFunc( basename, i ) )
268
			i += 1
270
			if not testns.exists():
271
				return testns
273
		raise AssertionError("Should never get here")
275
	def exists( self ):
276
		""":return: True if this namespace exists"""
277
		return cmds.namespace( ex=self )
279
	def isAbsolute( self ):
280
		"""
281
		:return: True if this namespace is an absolut one, defining a namespace
282
			from the root namespace like ":foo:bar"""
283
		return self.startswith( self._sep )
285
	def toRelative( self ):
286
		""":return: a relative version of self, thus it does not start with a colon
287
		:note: the root namespace cannot be relative - if this is of interest for you,
288
			you have to check for it. This method gracefully ignores that fact to make
289
			it more convenient to use as one does not have to be afraid of exceptions"""
290
		if not self.startswith( ":" ):
291
			return self.__class__( self )	# create a copy
293
		return self.__class__( self[1:], absolute=False )
295
	def relativeTo( self, basenamespace ):
296
		"""returns this namespace relative to the given basenamespace
298
		:param basenamespace: the namespace to which the returned one should be
299
			relative too
300
		:raise ValueError: If this or basenamespace is not absolute or if no relative
301
			namespace exists
302
		:return: relative namespace"""
303
		if not self.isAbsolute() or not basenamespace.isAbsolute( ):
304
			raise ValueError( "All involved namespaces need to be absolute: " + self + " , " + basenamespace )
306
		suffix = self._sep
307
		if basenamespace.endswith( self._sep ):
308
			suffix = ''
309
		relnamespace = self.replace( str( basenamespace ) + suffix, '' )
310
		if relnamespace == self:
311
			raise ValueError( str( basenamespace ) + " is no base of " + str( self ) )
313
		return self.__class__( relnamespace, absolute = False )
315
	@classmethod
316
	def splitNamespace( cls, objectname ):
317
		"""Cut the namespace from the given  name and return a tuple( namespacename, objectname )
319
		:note: method assumes that the namespace starts at the beginning of the object"""
320
		if objectname.find( '|' ) > -1:
321
			raise AssertionError( "Dagpath given where object name is required" )
323
		rpos = objectname.rfind( Namespace._sep )
324
		if rpos == -1:
325
			return ( Namespace.rootpath, objectname )
327
		return ( cls( objectname[:rpos] ), objectname[rpos+1:] )
330
	def _removeDuplicateSep( self, name ):
331
		""":return: name with duplicated : removed"""
332
		return self.re_find_duplicate_sep.sub( self._sep, name )
334
	def substitute( self, find_in, replacement ):
335
		"""
336
		:return: string with our namespace properly substituted with replacement such
337
			that the result is a properly formatted object name ( with or without namespace
338
			depending of the value of replacement )
339
			As this method is based on string replacement, self might as well match sub-namespaces
340
			if it is relative
341
		:note: if replacement is an empty string, it will effectively cut the matched namespace
342
			off the object name
343
		:note: handles replacement of subnamespaces correctly as well
344
		:note: as it operates on strings, the actual namespaces do not need to exist"""
346
		if self == Namespace.rootpath:
347
			return self._removeDuplicateSep( self.__class__( replacement, absolute=False ) + find_in )
350
		return self._removeDuplicateSep( find_in.replace( self, replacement ) )
352
	@classmethod
353
	def substituteNamespace( cls, thisns, find_in, replacement ):
354
		"""Same as `substitute`, but signature might feel more natural"""
355
		return thisns.substitute( find_in, replacement )
362
	def iterNodes( self, *args, **kwargs ):
363
		"""Return an iterator on all objects in the namespace
365
		:param args: MFn.kType filter types to be used to pre-filter the nodes 
366
			in the namespace. This can greatly improve performance !
367
		:param kwargs: given to `iterDagNodes` or `iterDgNodes`, which includes the 
368
			option to provide a predicate function. Additionally, the following ones 
369
			may be defined:
371
			 * asNode: 
372
			 	if true, default True, Nodes will be yielded. If False, 
373
			 	you will receive MDagPaths or MObjects depending on the 'dag' kwarg
375
			 * dag: 
376
			 	if True, default False, only dag nodes will be returned, otherwise you will 
377
				receive dag nodes and dg nodes. Instance information will be lost on the way
378
				though.
380
			 * depth: 
381
			 	if 0, default 0, only objects in this namespace will be returned
383
				if -1, all subnamespaces will be included as well, the depth is unlimited
385
				if 0<depth<x include all objects up to the 'depth' subnamespace
386
		:note: this method is quite similar to `FileReference.iterNodes`, but 
387
			has a different feature set and needs this code here for maximum performance"""
388
		import nt
389
		dag = kwargs.pop('dag', False)
390
		asNode = kwargs.get('asNode', True)
391
		predicate = kwargs.pop('predicate', lambda n: True)
392
		depth = kwargs.pop('depth', 0)
395
		kwargs['asNode'] = False
396
		pred = None
397
		iter_type = None
398
		nsGetRelativeTo = type(self).relativeTo
399
		selfrela = self.toRelative()+':'
400
		if dag:
401
			mfndag = api.MFnDagNode()
402
			mfndagSetObject = mfndag.setObject
403
			mfndagParentNamespace = mfndag.parentNamespace
405
			def check_filter(n):
406
				mfndagSetObject(n)
407
				ns = mfndagParentNamespace()
408
				if not _isRootOf(selfrela, ns):
409
					return False
412
				if depth > -1:
413
					ns = Namespace(ns)
414
					if self == ns:		# its depth 0
415
						return True
418
					if nsGetRelativeTo(ns, self).count(':')+1 > depth:
419
						return False
421
				return True
424
			iter_type = nt.it.iterDagNodes
425
			pred = check_filter
426
		else:
427
			iter_type = nt.it.iterDgNodes
428
			mfndep = api.MFnDependencyNode()
429
			mfndepSetObject = mfndep.setObject
430
			mfndepParentNamespace = mfndep.parentNamespace
432
			def check_filter(n):
433
				mfndepSetObject(n)
434
				ns = mfndepParentNamespace()
435
				if not _isRootOf(selfrela, ns):
436
					return False
441
				if depth > -1:
442
					ns = Namespace(ns)
443
					if self == ns:		# its depth 0
444
						return True
446
					if nsGetRelativeTo(ns, self).count(':')+1 > depth:
447
						return False
449
				return True
451
			iter_type = nt.it.iterDgNodes
452
			pred = check_filter
456
		kwargs['predicate'] = pred
457
		NodeFromObj = nt.NodeFromObj
458
		for n in iter_type(*args, **kwargs):
459
			if asNode:
460
				n = NodeFromObj(n)
461
			if predicate(n):
462
				yield n
468
def createNamespace( *args ):
469
	"""see `Namespace.create`"""
470
	return Namespace.create( *args )
472
def currentNamespace( ):
473
	"""see `Namespace.current`"""
474
	return Namespace.current()
476
def findUniqueNamespace( *args, **kwargs ):
477
	"""see `Namespace.findUnique`"""
478
	return Namespace.findUnique( *args, **kwargs )
480
def existsNamespace( namespace ):
481
	""":return : True if given namespace ( name ) exists"""
482
	return Namespace( namespace ).exists()
485
RootNamespace = Namespace(Namespace.rootpath)