mrv.mdepparse
Covered: 151 lines
Missed: 7 lines
Skipped 65 lines
Percent: 95 %
  2
"""Contains parser allowing to retrieve dependency information from maya ascii files
  3
and convert it into an easy-to-use networkx graph with convenience methods.
  4
"""
  5
__docformat__ = "restructuredtext"
  7
from networkx import DiGraph, NetworkXError
  8
from util import iterNetworkxGraph
  9
from path import make_path
 11
import sys
 12
import os
 13
import re
 15
import logging
 16
log = logging.getLogger("mrv.mdepparse")
 18
class MayaFileGraph( DiGraph ):
 19
	"""Contains dependnecies between maya files including utility functions
 20
	allowing to more easily find what you are looking for"""
 21
	kAffects,kAffectedBy = range( 2 )
 23
	refpathregex = re.compile( '.*-r .*"(.*)";' )
 25
	invalidNodeID = "__invalid__"
 26
	invalidPrefix = ":_iv_:"
 29
	@classmethod
 30
	def createFromFiles( cls, fileList, **kwargs ):
 31
		""":return: MayaFileGraph providing dependency information about the files
 32
			in fileList and their subReference.
 33
		:param fileList: iterable providing the filepaths to be parsed and added
 34
			to this graph
 35
		:param kwargs: alll arguemnts of `addFromFiles` are supported """
 36
		graph = cls( )
 37
		graph.addFromFiles( fileList, **kwargs )
 38
		return graph
 41
	def _addInvalid( self, invalidfile ):
 42
		"""Add an invalid file to our special location
 43
		:note: we prefix it to assure it does not popup in our results"""
 44
		self.add_edge( self.invalidNodeID, self.invalidPrefix + str( invalidfile ) )
 46
	@classmethod
 47
	def _parseReferences( cls, mafile, allPaths = False ):
 48
		""":return: list of reference strings parsed from the given maya ascii file
 49
		:raise IOError: if the file could not be read"""
 50
		outrefs = list()
 51
		filehandle = open( os.path.expandvars( mafile ), "r" )
 53
		num_rdi_paths = 0		# amount of -rdi paths we found - lateron we have to remove the items
 57
		for line in filehandle:
 60
			line = line.strip()
 61
			if not line.endswith( ";" ):
 62
				try:
 63
					line = line + filehandle.next()
 64
				except StopIteration:
 65
					break
 68
			match = cls.refpathregex.match( line )
 70
			if match:
 71
				outrefs.append( match.group(1) )
 75
			if not allPaths and line.startswith( "requires" ):
 76
				break
 79
		filehandle.close()
 81
		return outrefs
 83
	def _parseDepends( self, mafile, allPaths ):
 84
		""":return: list of filepath as parsed from the given mafile.
 85
		:param allPaths: if True, the whole file will be parsed, if False, only
 86
			the reference section will be parsed"""
 87
		outdepends = list()
 88
		log.info("Parsing %s" % ( mafile ))
 90
		try:
 91
			outdepends = self._parseReferences( mafile, allPaths )
 92
		except IOError,e:
 94
			self._addInvalid( mafile )
 95
			log.warn("Parsing Failed: %s" % str( e ))
 97
		return outdepends
100
	def addFromFiles( self, mafiles, parse_all_paths = False,
101
					to_os_path = lambda f: make_path(f).expandvars(),
102
					os_path_to_db_key = lambda f: f):
103
		"""Parse the dependencies from the given maya ascii files and add them to
104
		this graph
106
		:note: the more files are given, the more efficient the method can be
107
		:param parse_all_paths: if True, default False, all paths found in the file will be used.
108
			This will slow down the parsing as the whole file will be searched for references
109
			instead of just the header of the file
110
		:param to_os_path: functor returning an MA file from given posssibly parsed file
111
			that should be existing on the system parsing the files.
112
			The passed in file could also be an mb file ( which cannot be parsed ), thus it
113
			would be advantageous to return a corresponding ma file
114
			This is required as references can have environment variables inside of them
115
		:param os_path_to_db_key: converts the given path as used in the filesystem into
116
			a path to be used as key in the database. It should be general.
117
			Ideally, os_path_to_db_key is the inverse as to_os_path.
118
		:note: if the parsed path contain environment variables you must start the
119
			tool such that these can be resolved by the system. Otherwise files might
120
			not be found
121
		:todo: parse_all_paths still to be implemented"""
122
		files_parsed = set()					 # assure we do not duplicate work
123
		for mafile in mafiles:
124
			depfiles = [ mafile.strip() ]
125
			while depfiles:
126
				curfile = to_os_path( depfiles.pop() )
129
				if os.path.splitext( curfile )[1] != ".ma":
130
					log.info( "Skipped non-ma file: %s" % curfile )
131
					continue
134
				if curfile in files_parsed:
135
					continue
137
				curfiledepends = self._parseDepends( curfile, parse_all_paths )
138
				files_parsed.add( curfile )
141
				curfilestr = str( curfile )
142
				valid_depends = list()
143
				for depfile in curfiledepends:
144
					print depfile
146
					dbdepfile = to_os_path( depfile )
147
					print dbdepfile
148
					if os.path.exists( dbdepfile ):
149
						valid_depends.append( depfile )				# store the orig path - it will be converted later
150
						dbdepfile = os_path_to_db_key( dbdepfile )		# make it db key path
151
					else:
152
						dbdepfile = depfile								# invalid - revert it
153
						self._addInvalid( depfile )						# store it as invalid, no further processing
155
					self.add_edge( dbdepfile, os_path_to_db_key( curfilestr ) )
158
				depfiles.extend( valid_depends )
165
	def depends( self, filePath, direction = kAffects,
166
				   to_os_path = lambda f: os.path.expandvars( f ),
167
					os_path_to_db_key = lambda f: f, return_unresolved = False,
168
				   invalid_only = False, **kwargs ):
169
		""":return: list of paths ( converted to os paths ) that are related to
170
			the given filePath
171
		:param direction: specifies search direction, either :
172
			kAffects = Files that filePath affects
173
			kAffectedBy = Files that affect filePath
174
		:param return_unresolved: if True, the output paths will not be translated to
175
			an os paths and you get the paths as stored in the graph.
176
			Please not that the to_os_path function is still needed to generate
177
			a valid key, depending on the format of filepaths stored in this graph
178
		:param invalid_only: if True, only invalid dependencies will be returned, all
179
			including the invalid ones otherwise
180
		:param to_os_path: see `addFromFiles`
181
		:param os_path_to_db_key: see `addFromFiles`
182
		:param kwargs: passed to `iterNetworkxGraph`"""
183
		kwargs[ 'direction' ] = direction
184
		kwargs[ 'ignore_startitem' ] = 1			# default
185
		kwargs[ 'branch_first' ] = 1		# default
187
		keypath = os_path_to_db_key( to_os_path( filePath ) )	# convert key
188
		invalid = set( self.invalidFiles() )
190
		if return_unresolved:
191
			to_os_path = lambda f: f
193
		outlist = list()
195
		try:
196
			for d, f in iterNetworkxGraph( self, keypath, **kwargs ):
197
				is_valid = f not in invalid
198
				f = to_os_path( f )		# remap only valid paths
200
				if is_valid and invalid_only:	# skip valid ones ?
201
					continue
203
				outlist.append( f )
205
		except NetworkXError:
206
			log.debug( "Skipped Path %s ( %s ): unknown to dependency graph" % ( filePath, keypath ) )
208
		return outlist
210
	def invalidFiles( self ):
211
		"""
212
		:return: list of filePaths that could not be parsed, most probably
213
			because they could not be found by the system"""
214
		lenp = len( self.invalidPrefix  )
216
		try:
217
			return [ iv[ lenp : ] for iv in self.successors( self.invalidNodeID ) ]
218
		except NetworkXError:
219
			return list()