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.
5
__docformat__ = "restructuredtext"
7
from networkx import DiGraph, NetworkXError
8
from util import iterNetworkxGraph
9
from path import make_path
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_:"
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
35
:param kwargs: alll arguemnts of `addFromFiles` are supported """
37
graph.addFromFiles( fileList, **kwargs )
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 ) )
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"""
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
54
# at the respective position as both lists match
57
for line in filehandle:
59
# take the stupid newlines into account !
61
if not line.endswith( ";" ):
63
line = line + filehandle.next()
66
# END newline special handling
68
match = cls.refpathregex.match( line )
71
outrefs.append( match.group(1) )
74
# see whether we can abort early
75
if not allPaths and line.startswith( "requires" ):
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"""
88
log.info("Parsing %s" % ( mafile ))
91
outdepends = self._parseReferences( mafile, allPaths )
94
self._addInvalid( mafile )
95
log.warn("Parsing Failed: %s" % str( e ))
96
# END exception handlign
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
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
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() ]
126
curfile = to_os_path( depfiles.pop() )
129
if os.path.splitext( curfile )[1] != ".ma":
130
log.info( "Skipped non-ma file: %s" % curfile )
134
if curfile in files_parsed:
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:
145
# only valid files may be adjusted - we keep them as is otherwise
146
dbdepfile = to_os_path( depfile )
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
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 ) )
157
# add to stack and go on
158
depfiles.extend( valid_depends )
159
# END dependency loop
160
# END for each file to parse
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
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
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 ?
204
# END for each file in dependencies
205
except NetworkXError:
206
log.debug( "Skipped Path %s ( %s ): unknown to dependency graph" % ( filePath, keypath ) )
210
def invalidFiles( self ):
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 )
217
return [ iv[ lenp : ] for iv in self.successors( self.invalidNodeID ) ]
218
except NetworkXError:
220
# END no invalid found exception handling