This has some stuff specific to the layout of our project but most of it is steal-able for any structure. 99 times out of a hundred I just make a project.ULProject.default() object and use it for all path management in an area . In rare cases I make two or more explicit ones using the root, project, branch constructor so I could, eg, map between two branches which had different directory hierarchies
The DepotProject class does the exact same stuff but for perforce so you can use it to, say, bulk-relativize a bunch of depot file paths into local disk paths
'''
classes and methods for handling the UL project structure
'''
from os import environ, getcwd, chdir, pathsep, path, mkdir #@UnusedImport
import posixpath #@UnresolvedImport
from collections import deque
class ProjectError ( ValueError ):
pass
class OutOfProjectError( ProjectError ):
pass
class ULProject( object ):
'''
Represents a combination of a root directory , a project directory, and a branch which together define a working branch of the UL project tree
'''
VALID_PROJECTS = ['class3', 'class4']
CONTENT_ROOT = ['Game/', 'game/', 'GAME/'] # precased for speed
MAYA_TOOLS_LOCATION = ["tools", "dcc", "maya"]
def __init__( self, root, project, branch ):
self._Root = root
self._Project = project
self._Branch = branch
@property
def Root( self ): return self._Root
@property
def Project( self ): return self._Project
@property
def Branch( self ): return self._Branch
@property
def Content( self ): return path.join( self.Path, self.CONTENT_ROOT[0] ).replace( "\\", "/" )
@property
def Path( self ):
rootPath = path.join( self.Root, self.Project, self.Branch )
return rootPath.replace( "\\", "/" )
def __str__( self ):
return self.Path
def __repr__( self ):
return "< project : %s>" % path.join( self.Project, self.Branch )
def contains( self, abspath ):
'''
Returns true if the supplied path is contained in this project
Any non-absolute path is assumed to be contained, so all relative paths return true.
@note: this DOES NOT check the disk -- it just indicated if the path is formally part of the project.
'''
# relative paths are presumed to be 'contained'
abspath = abspath.replace( "\\", "/" ).lstrip( "/" )
if not path.isabs( abspath ): return True #@UndefinedVariable
p1 = path.normcase( path.normpath( self.Path ) )
p2 = path.normcase( path.normpath( abspath ) )
return path.commonprefix( [p1, p2] ) == p1
def relative( self, abspath ):
'''
Returns an absolute path as a path relative to the project root.
if abspath is not absolute (ie, no disk path) it's returned unchanged
'''
# relative paths are returned unchanged
abspath = abspath.replace( "\\", "/" ).lstrip( "/" )
if not path.isabs( abspath ): return abspath #@UndefinedVariable
if ( self.contains( abspath ) ):
relpath = path.relpath( abspath, self.Path )
return path.normpath( relpath ).replace( path.sep, posixpath.sep )
else:
raise OutOfProjectError( "%s is not inside project %s " % ( abspath, self.Path ) )
def absolute( self, *relpath ):
'''
Return the supplied relative path as an absolute path
If multiple items are supplied they are concatenated to make a path:
>>> proj.absolute('rel/path')
>>> 'C:/ul/class3/main/rel/path'
>>> proj.absoluter('rel', 'path', 'segs')
>>> 'C:/ul/class3/main/rel/path/segs'
'''
if '//depot' in relpath[0].lower():
raise ValueError, 'Do not use ULProject instance with depot paths -- try DepotProject instance instead'
# lstrip -- otherwise left slashes are 'absolute' and don't concatenate
noSlash = lambda q: q.strip( "\\/" ).strip( "\\/" )
argList = map ( noSlash, list( relpath ) )
absCount = 0;
for item in argList:
if path.isabs( item ): absCount += 1
if ( absCount > 1 ) : raise ProjectError( "supplied items contain more than one absolute path : %s" % ( ", ".join( argList ) ) )
if ( absCount == 1 ):
return posixpath.normpath( posixpath.join( *argList ) ).replace( '\\', '/' )
else:
argList.insert( 0, self.Path )
return posixpath.normpath( posixpath.join( *argList ) ).replace( '\\', '/' )
def asset_path( self, path ):
'''
Returns relative path to a file in the content directory (typically /Game/)
'''
relpath = self.relative( path )
for item in self.CONTENT_ROOT:
if relpath.startswith( item ): return relpath[len( item ):]
return relpath
def absolute_asset_path( self, relpath ):
'''
Returns an absolute path for a path relative to the content root (ie, /game/ ) directory
'''
if path.isabs( relpath ):
raise ProjectError( "absolute asset path expects a path relative to the content root directory, got '%s'" % relpath )
segments = list( path.split( relpath ) )
c_root = self.CONTENT_ROOT[0].rstrip( "/" )
if segments[0].lower() != c_root.lower():
segments.insert( 0, c_root )
return self.absolute( *segments )
def maya_tools( self ):
toolsPath = path.join( self.Path, *self.MAYA_TOOLS_LOCATION )
return self.absolute( toolsPath )
def same( self, path1, path2 ):
'''
Compares two paths, returns true if they absolutize to the same value. Comparison is case insensitive
'''
ab = self.absolute( path1 )
ab2 = self.absolute( path2 )
return self._pathCompare( ab, ab2 )
def _pathCompare( self, path1, path2 ):
p1 = path.normcase( path.normpath( path1 ) )
p2 = path.normcase( path.normpath( path2 ) )
return p1 == p2
def __eq__( self, other ):
'''
Equality operator. If two project objects share the same branch and same project, they are 'equal'
'''
try:
return self.Project.lower() == other.Project.lower() and self.Branch.lower() == other.Branch.lower()
except:
return False
@classmethod
def default( cls ):
return cls( environ["UL"], environ["UL_PROJECT"], environ["UL_BRANCH"] )
@staticmethod
def from_path( filepath ):
filepath = filepath.replace( '/', path.sep )
pathSegs = filepath.split( path.sep )
for n in range( len( pathSegs ) - 1, 0 , -1 ):
if ULProject.VALID_PROJECTS.count( pathSegs[n].lower() ):
root = path.sep.join( pathSegs[:n] )
project = pathSegs[n]
branch = pathSegs[n + 1]
return ULProject( root, project, branch )
raise OutOfProjectError ( "%s is not in a recognized project path" % filepath )
class DepotProject( ULProject ):
'''
This subclass of ULProject exposes the same functions, however the paths are
relativized and absolutized to p4 depot paths. Thus, calling absolute
returns something like '//depot/class3/main/path/segments', and calling
relative will turn //depot/class3/main/relative/path' into 'relative/path'
'''
def __init__( self, root, project, branch ):
self._Root = root
self._Project = project
self._Branch = branch
self._Depot = '//depot'
self._InvalidDepot = '\\depot'
@property
def Depot( self ):
return self._Depot
@property
def DepotPath( self ):
return posixpath.join( self.Depot, self.Project, self.Branch )
def contains( self, abspath ):
'''
returns true if abspath is contained in this project. Abspath can be either a disk path or an depot path
'''
# relative paths are presumed to be 'contained'
abspath = abspath.lstrip( '\\' )
abspath = abspath.replace( "\\", "/" )
if not path.isabs( abspath ): return True #@UndefinedVariable
p1 = None
if ":" in abspath:
p1 = path.normcase( posixpath.normpath( self.Path ) )
else:
p1 = path.normcase( posixpath.normpath( self.DepotPath ) )
p2 = path.normcase( posixpath.normpath( abspath ) )
return path.commonprefix( [p1, p2] ) == p1
def absolute( self, *relpath ):
'''
Return the supplied relative path as an absolute path
If multiple items are supplied they are concatenated to make a path:
>>> proj.absolute('rel/path')
>>> '//depot/class3/main/rel/path'
>>> proj.absoluter('rel', 'path', 'segs')
>>> //depot/class3/main/rel/path/segs'
'''
if self.Depot.lower() in relpath[0].lower(): # this means we are a depot path
val = posixpath.normpath( posixpath.join( *relpath ) )
if not val.startswith( '//' ): val = "/" + val # special-case handling in case somebody passed in '///depot'
return val.replace( '\\', '/' )
if self._InvalidDepot.lower() in relpath[0].lower():
raise ProjectError, "Depot paths must start with //depot (no left slashes)"
noSlash = lambda q: q.lstrip( "\\/" )
argList = map ( noSlash, list( relpath ) )
absCount = 0;
for item in argList:
if path.isabs( item ): absCount += 1
if ( absCount > 1 ) : raise ProjectError( "supplied items contain more than one absolute path : %s" % ( ", ".join( argList ) ) )
argList.insert( 0, self.DepotPath )
return posixpath.normpath( posixpath.join( *argList ) ).replace( '\\', '/' )
def relative( self, abspath ):
'''
Returns an absolute depot path as a path relative to the project root. Abspath can be a depot path or a disk path
if abspath is not absolute (ie, no disk path) it's returned unchanged
'''
# relative paths are returned unchanged
abspath = abspath.replace( "\\", "/" )
if ":" in abspath:
return ULProject.relative( self, abspath )
if not posixpath.isabs( abspath ): return abspath #@UndefinedVariable
if ( self.contains( abspath ) ):
relpath = path.relpath( abspath, self.DepotPath )
return relpath.replace( path.sep, posixpath.sep )
else:
raise OutOfProjectError( "%s is not inside project %s " % ( abspath, self.Path ) )
def local( self, depotpath ):
'''
returns the supplied path as a file system path:
>>> proj.local('game')
>>> 'C:/UL/Class3/Main/game'
>>>
>>> proj.local('//depo/class3/main/game')
>>> 'C:/UL/Class3/main/game'
@note: the mapping DOES NOT use the perforce client spec to do this
translation! It just assumes that the depot is mapped as
UL/UL_PROJECT/UL_BRANCH -- which is typically safe but might not
work properly if the client mapping is not standard. USE WITH CARE.
'''
relpath = self.relative( depotpath )
return ULProject.absolute( self, relpath )
def to_depot( self, diskpath ):
'''
Given a disk path or a relative path, returns a depot path
>>> proj.to_depot('C:/UL/Class3/main/game')
>>> '//depot/Class3/main/game'
>>> proj.to_depot('game')
>>> '//depot/Class3/main/game'
@note: DOES NOT use p4 client to establish mapping. USE WITH CARE
'''
dp = ULProject.relative( self, diskpath )
return self.absolute( dp )
def __repr__( self ):
return "< DepotProject : %s>" % path.join( self.Project, self.Branch )
def create_path( newpath ):
'''
Creates all of the directories needed to complete a path
If the final entry of the path contains a "." character it is treated as a file and ignored
If the path cannot be created, raise a WindowsError
'''
newpath = newpath.replace( "\\", '/' )
newpath = posixpath.normpath( newpath )
newpath = newpath.lstrip( '/' )
if not path.isabs( newpath ):
raise ValueError, 'path %s is not absolute' % newpath
# remove the file, if there is one
finalpath, tail = posixpath.split( newpath )
if not ( "." ) in tail:
finalpath = newpath
segs = deque( finalpath.split( posixpath.sep ) )
done = []
trunk = ""
while segs:
done.append( segs.popleft() )
trunk = "/".join( done )
if not path.exists( trunk ):
mkdir( trunk )