mrv.maya.ref
Covered: 385 lines
Missed: 10 lines
Skipped 168 lines
Percent: 97 %
  2
"""
  3
Allows convenient access and handling of references in an object oriented manner
  4
"""
  5
__docformat__ = "restructuredtext"
  7
from mrv.path import make_path
  8
from mrv.util import And
  9
from mrv.exc import MRVError
 10
from mrv.maya.ns import Namespace, _isRootOf
 11
from mrv.maya.util import noneToList
 12
from mrv.interface import iDagItem
 13
import undo
 14
import maya.cmds as cmds
 15
import maya.OpenMaya as api
 16
from itertools import ifilter
 18
__all__ = ("createReference", "listReferences", "FileReference", "FileReferenceError")
 21
class FileReferenceError(MRVError):
 22
	pass
 29
def createReference(*args, **kwargs):
 30
	"""create a new reference, see `FileReference.create` for more information"""
 31
	return FileReference.create(*args, **kwargs)
 33
def listReferences(*args, **kwargs):
 34
	"""List intermediate references of in the scene, see `FileReference.ls` for 
 35
	more information"""
 36
	return FileReference.ls(*args, **kwargs)
 41
class FileReference(iDagItem):
 42
	"""Represents a Maya file reference
 44
	:note: do not cache these instances but get a fresh one when you have to work with it
 45
	:note: as FileReference is also a iDagItem, all the respective methods, especially for
 46
		parent/child iteration and query can be used as well"""
 47
	editTypes = [	'setAttr','addAttr','deleteAttr','connectAttr','disconnectAttr','parent']
 48
	_sep = '/'					# iDagItem configuration
 49
	__slots__ = '_refnode'
 51
	@classmethod
 52
	def _splitCopyNumber(cls, path):
 53
		""":return: (path, copynumber), copynumber is at least 0 """
 54
		lbraceindex = path.rfind('{')
 55
		if lbraceindex == -1:
 56
			return (path, 0)
 59
		return (path[:lbraceindex], int(path[lbraceindex+1:-1]))
 62
	def __init__(self, filepath = None, refnode = None):
 63
		if refnode:
 64
			self._refnode = str(refnode)
 65
		elif filepath:
 66
			self._refnode = cmds.referenceQuery(filepath, rfn=1)
 67
		else:
 68
			raise ValueError("Specify either filepath or refnode to initialize the instance from")
 71
	def __eq__(self, other):
 72
		"""Special treatment for other filereferences"""
 74
		if isinstance(other, FileReference):
 75
			return self._refnode == other._refnode
 77
		return self.path() == other
 79
	def __ne__(self, other):
 80
		return not self.__eq__(other)
 82
	def __hash__(self):
 83
		return hash(self.path(copynumber=1))
 85
	def __str__(self):
 86
		return str(self.path())
 88
	def __repr__(self):
 89
		return "FileReference(%s)" % str(self.path(copynumber=1))
 94
	@classmethod
 95
	def create(cls, filepath, namespace=None, load = True, **kwargs):
 96
		"""Create a reference with the given namespace
 98
		:param filepath: path describing the reference file location
 99
		:param namespace: if None, a unique namespace will be generated for you
100
			The namespace will contain all referenced objects.
101
		:param load: if True, the reference will be created in loaded state, other
102
			wise its loading is deferred
103
		:param kwargs: passed to file command
104
		:raise ValueError: if the namespace does already exist
105
		:raise RuntimeError: if the reference could not be created"""
106
		filepath = make_path(cls._splitCopyNumber(filepath)[0])
108
		def nsfunc(base, i):
109
			if not i: return base
110
			return "%s%i" % (base,i)
112
		ns = namespace
113
		if not ns:										# assure unique namespace
114
			nsbasename = filepath.stripext().basename()
115
			ns = Namespace.findUnique(nsbasename, incrementFunc = nsfunc)
116
		else:
117
			ns = Namespace(ns)		# assure we have a namespace object
119
		ns = ns.relativeTo(Namespace(Namespace.rootpath))
120
		if ns.exists():
121
			raise ValueError("Namespace %s for %s does already exist" % (ns,filepath))
124
		prevns = Namespace.current()
127
		kwargs.pop('ns', None)
128
		kwargs.pop('reference', kwargs.pop('r', None))
129
		kwargs.pop('deferReference', kwargs.pop('dr', None))
130
		try:
131
			createdRefpath = cmds.file(filepath, ns=str(ns),r=1,dr=not load, **kwargs)
132
		finally:
133
			prevns.setCurrent()
136
		return FileReference(createdRefpath)
138
	@undo.notundoable
139
	def remove(self, **kwargs):
140
		""" Remove the given reference from the scene
142
		:note: assures that no namespaces of that reference are left, remaining objects
143
			will be moved into the root namespace. This way the namespaces will not be left as waste.
144
			This fails if there are referenced objects in the subnamespace - we currently 
145
			ignore that issue as the main reference removal worked at that point.
146
		:note: kwargs passed to namespace.delete """
147
		ns = self.namespace()
148
		cmds.file(self.path(copynumber=1), rr=1)
149
		try:
150
			ns.delete(**kwargs)
151
		except RuntimeError:
152
			pass
154
	@undo.notundoable
155
	def replace(self, filepath):
156
		"""Replace this reference with filepath
158
		:param filepath: the path to the file to replace this reference with
159
			Reference instances will be handled as well.
160
		:return: self"""
161
		filepath = (isinstance(filepath, type(self)) and filepath.path()) or filepath
162
		filepath = self._splitCopyNumber(filepath)[0]
163
		cmds.file(filepath, lr=self._refnode)
164
		return self
166
	@undo.notundoable
167
	def importRef(self, depth=0):
168
		"""Import the reference until the given depth is reached
170
		:param depth:
171
			 - x<1: import all references and subreferences
172
			 - x: import until level x is reached, 0 imports just self such that
173
			 	all its children are on the same level as self was before import
174
		:return: list of FileReference objects that are now in the root namespace - this
175
			  list could be empty if all subreferences are fully imported"""
176
		def importRecursive(reference, curdepth, maxdepth):
178
			reference.setLoaded(True)
179
			children = reference.children()
180
			cmds.file(reference.path(copynumber=1), importReference=1)
182
			if curdepth == maxdepth:
183
				return children
185
			outsubrefs = list()
186
			for childref in children:
187
				outsubrefs.extend(importRecursive(childref, curdepth+1, maxdepth))
189
			return outsubrefs
192
		return importRecursive(self, 0, depth)
198
	@classmethod
199
	def fromPaths(cls, paths, **kwargs):
200
		"""Find the reference for each path in paths. If you provide the path X
201
		2 times, but you only have one reference to X, the return value will be 
202
		[FileReference(X), None] as there are less references than provided paths.
204
		:param paths: a list of paths or references whose references in the scene 
205
			should be returned. In case a reference is found, its plain path will be 
206
			used instead.
207
		:param kwargs: all supported by `ls` to yield the base set of references
208
			we will use to match the paths with. Additionally, you may specify:
210
			 * ignore_extension: 
211
			 	if True, default False, the extension will be ignored
212
				during the search, only the actual base name will matter.
213
				This way, an MA file will be matched with an MB file. 
214
				The references returned will still have their extension original extension.
216
		:return: list(FileReference|None, ...)
217
			if a filereference was found for given occurrence of Path, it will be returned
218
			at index of the current path in the input paths, otherwise it is None.
219
		:note: zip(paths, result) to get a corresponding tuple list associating each input path
220
			with the located reference"""
221
		if not isinstance(paths, (list,tuple)) or hasattr(paths, 'next'):
222
			raise TypeError("paths must be tuple, was %s" % type(paths))
224
		ignore_ext = kwargs.pop("ignore_extension", False)
225
		refs = cls.ls(**kwargs)
229
		lut = dict()
230
		pathscp = [(isinstance(p, cls) and p.path()) or make_path(p) for p in paths]
232
		conv = lambda f: f
233
		if ignore_ext:
234
			conv = lambda f: f.expandvars().splitext()[0]
237
		def countTuple(filepath, lut):
238
			count = lut.get(filepath, 0)
239
			lut[filepath] = count + 1
240
			return (filepath , count)
243
		clut = dict()
244
		for ref in refs:
245
			lut[countTuple(conv(ref.path()), clut)] = ref			# keys have no ext
248
		clut.clear()
249
		for i,path in enumerate(pathscp):
250
			pathscp[i] = countTuple(conv(path), clut)
253
		outlist = list()
254
		for path in pathscp:
255
			ref_or_none = lut.get(path, None)
256
			outlist.append(ref_or_none)
259
		return outlist
261
	@classmethod
262
	def ls(cls, rootReference = "", predicate = lambda x: True):
263
		"""list all references in the scene or under the given root
265
		:param rootReference: if not empty, the references below it will be returned.
266
			Otherwise all scene references will be listed.
267
			May be string, Path or FileReference
268
		:param predicate: method returning true for each valid file reference object that 
269
			should be part of the return value.
270
		:return: list of `FileReference` s objects"""
271
		if isinstance(rootReference, cls):
272
			rootReference = rootReference.path(copynumber=1)
274
		out = list()
275
		for reffile in cmds.file(str(rootReference), q=1, r=1):
276
			refinst = FileReference(filepath = reffile)
277
			if predicate(refinst):
278
				out.append(refinst)
280
		return out
282
	@classmethod
283
	def lsDeep(cls, predicate = lambda x: True, **kwargs):
284
		"""Return all references recursively
286
		:param kwargs: support for arguments as in `ls`, hence you can use the 
287
			rootReference flag to restrict the set of returned FileReferences."""
288
		kwargs['predicate'] = predicate
289
		refs = cls.ls(**kwargs)
290
		out = refs
291
		for ref in refs:
292
			out.extend(ref.childrenDeep(order=cls.kOrder_BreadthFirst, predicate=predicate))
293
		return out
298
	def iterNodes(self, *args, **kwargs):
299
		"""Creates iterator over nodes in this reference
301
		:param args: MFn.kType filter ids to be used to pre-filter all nodes.
302
			If you know what you are looking for, setting this can greatly improve 
303
			performance !
304
		:param kwargs: additional kwargs will be passed to either `iterDagNodes`
305
			or `iterDgNodes` (dag = False). The following additional kwargs may
306
			be specified:
308
			 * asNode: 
309
			 	if True, default True, return wrapped Nodes, if False MDagPaths
310
			 	or MObjects will be returned
312
			 * dag: 
313
			 	if True, default False, return dag nodes only. Otherwise return dependency nodes 
314
			 	as well. Enables assemblies and assembilesInReference.
316
			 * assemblies: 
317
			 	if True, return only dagNodes with no parent. Needs dag and 
318
			 	is mutually exclusive with assembilesInReference.
320
			 * assembliesInReference: 
321
			 	if True, return only dag nodes that have no
322
				parent in their own reference. They may have a parent not coming from their
323
				reference though. This flag has a big negative performance impact and requires dag.
325
			 * predicate: 
326
			 	if function returns True for Node|MObject|MDagPath n, n will be yielded.
327
			 	Defaults to return True for all.
328
		:raise ValueError: if incompatible arguments have been given"""
329
		import nt
331
		rns = self.namespace()
332
		rnsrela = rns.toRelative()+':'
333
		asNode = kwargs.get('asNode', True)
334
		predicate = kwargs.get('predicate', lambda n: True)
335
		kwargs['asNode'] = False	# we will do it
337
		dag = kwargs.pop('dag', False)
338
		assemblies = kwargs.pop('assemblies', False)
339
		assembliesInReference = kwargs.pop('assembliesInReference', False)
342
		if (assemblies or assembliesInReference) and not dag:
343
			raise ValueError("Cannot list assemblies of any kind if dag is not specified")
345
		if assemblies and assembliesInReference:
346
			raise ValueError("assemblies and assembilesInReference are mutually exclusive")
349
		iter_type = None
350
		pred = None
351
		if dag:
353
			mfndag = api.MFnDagNode()
354
			mfndagSetObject = mfndag.setObject
355
			mfndagParentNamespace = mfndag.parentNamespace
356
			MDagPath = api.MDagPath
357
			mdppop = MDagPath.pop
358
			mdplen = MDagPath.length
360
			def check_dag_ns(n):
361
				mfndagSetObject(n)
362
				if not _isRootOf(rnsrela, mfndagParentNamespace()):
363
					return False
367
				if assemblies: 
368
					nc = MDagPath(n)
369
					mdppop(nc, 1)
370
					if mdplen(nc) != 0:
371
						return False
373
				elif assembliesInReference:
374
					nc = MDagPath(n)
375
					mdppop(nc, 1)
376
					if mdplen(nc) != 0:
378
						mfndagSetObject(n)
379
						if _isRootOf(rnsrela, mfndagParentNamespace()):
380
							return False
384
				return True
387
			pred = check_dag_ns
388
			iter_type = nt.it.iterDagNodes
389
		else:
390
			mfndep = api.MFnDependencyNode()
391
			mfndepSetObject = mfndep.setObject
392
			mfndepParentNamespace = mfndep.parentNamespace
394
			def check_ns(n):
395
				mfndepSetObject(n)
396
				if not _isRootOf(rnsrela, mfndepParentNamespace()):
397
					return False
399
				return True
402
			pred = check_ns
403
			iter_type = nt.it.iterDgNodes
406
		kwargs['predicate'] = pred
409
		NodeFromObj = nt.NodeFromObj
410
		for n in iter_type(*args, **kwargs):
411
			if asNode:
412
				n = NodeFromObj(n)
413
			if predicate(n):
414
				yield n
419
	@undo.notundoable
420
	def cleanup(self, unresolvedEdits = True, editTypes = editTypes):
421
		"""remove unresolved edits or all edits on this reference
423
		:param unresolvedEdits: if True, only dangling connections will be removed,
424
			if False, all reference edits will be removed - the reference will be unloaded for beforehand.
425
			The loading state of the reference will stay unchanged after the operation.
426
		:param editTypes: list of edit types to remove during cleanup
427
		:return: self"""
428
		wasloaded = self.isLoaded()
429
		if not unresolvedEdits:
430
			self.setLoaded(False)
432
		for etype in editTypes:
433
			cmds.file(cr=self._refnode, editCommand=etype)
435
		if not unresolvedEdits:
436
			self.setLoaded(wasloaded)
438
		return self
440
	@undo.notundoable
441
	def setLocked(self, state):
442
		"""Set the reference to be locked or unlocked
444
		:param state: if True, the reference is locked , if False its unlocked and
445
			can be altered
446
		:return: self"""
447
		if self.isLocked() == state:
448
			return
451
		wasloaded = self.isLoaded()
452
		self.setLoaded(False)
455
		cmds.setAttr(self._refnode+".locked", state)
458
		self.setLoaded(wasloaded)
460
		return self
462
	@undo.notundoable
463
	def setLoaded(self, state):
464
		"""set the reference loaded or unloaded
466
		:param state: True = unload reference, True = load reference 
467
		:return: self"""
469
		if state == self.isLoaded():			# already desired state
470
			return
472
		if state:
473
			cmds.file(loadReference=self._refnode)
474
		else:
475
			cmds.file(unloadReference=self._refnode)
477
		return self
479
	@undo.notundoable
480
	def setNamespace(self, namespace):
481
		"""set the reference to use the given namespace
483
		:param namespace: Namespace instance or name of the short namespace
484
		:raise RuntimeError: if namespace already exists or if reference is not root
485
		:return: self"""
486
		shortname = namespace
487
		if isinstance(namespace, Namespace):
488
			shortname = namespace.basename()
491
		cmds.file(self.path(copynumber=1), e=1, ns=shortname)
493
		return self
497
	def parent(self):
498
		""":return: the parent reference of this instance or None if we are root"""
499
		parentrfn = cmds.referenceQuery(self._refnode, rfn=1, p=1)
500
		if not parentrfn:
501
			return None
502
		return FileReference(refnode = parentrfn)
504
	def children(self , predicate = lambda x: True):
505
		""" :return: all intermediate child references of this instance """
506
		return self.ls(rootReference = self, predicate = predicate)
509
	def exists(self):
510
		""":return: True if our file reference exists in maya"""
511
		try:
512
			self.path(copynumber=1)
513
		except RuntimeError:
514
			return False
515
		else:
516
			return True
518
	def isLocked(self):
519
		""":return: True if reference is locked"""
520
		return cmds.getAttr(self._refnode + ".locked")
522
	def isLoaded(self):
523
		""":return: True if the reference is loaded"""
524
		return cmds.file(rfn=self._refnode, q=1, dr=1) == False
526
	def copynumber(self):
527
		""":return: the references copy number - starting at 0 for the first reference
528
		:note: we do not cache the copy number as mayas internal numbering can change on
529
			when references change - the only stable thing is the reference node name"""
530
		return self._splitCopyNumber(self.path(copynumber=1))[1]
532
	def namespace(self):
533
		""":return: namespace object of the full namespace holding all objects in this reference"""
534
		fullpath = self.path(copynumber=1)
535
		refspace = cmds.file(fullpath, q=1, ns=1)
536
		parentspace = cmds.file(fullpath, q=1, pns=1)[0]		# returns lists, although its always just one string
537
		if parentspace:
538
			parentspace += ":"
540
		return Namespace(":" + parentspace + refspace)
542
	def path(self, copynumber=False, unresolved = False):
543
		""":return: Path object with the path containing the reference's data
544
		:param copynumber: If True, the returned path will include the copy number.
545
			As it will be a path object, it might not be fully usable in that state
546
		:param unresolved: see `ls`
547
		:note: we always query it from maya as our numbers change if some other
548
			reference is being removed and cannot be trusted"""
549
		path_str = cmds.referenceQuery(self._refnode, f=1, un=unresolved)
550
		if not copynumber:
551
			path_str = self._splitCopyNumber(path_str)[0]
553
		return make_path(path_str)
555
	def referenceNode(self):
556
		""":return: wrapped reference node managing this reference"""
557
		import mrv.maya.nt as nt
558
		return nt.NodeFromStr(self._refnode)