Another version of the relative path script already posted on the cookbook website. This one is somewhat shorter (at only 8 lines, excluding the data checks) and is (I hope!) clearer and more elegant. It also includes a unit testing script.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | #!/usr/bin/python
#
# relpath.py
# R.Barran 30/08/2004
import os
def relpath(target, base=os.curdir):
"""
Return a relative path to the target from either the current dir or an optional base dir.
Base can be a directory specified either as absolute or relative to current dir.
"""
if not os.path.exists(target):
raise OSError, 'Target does not exist: '+target
if not os.path.isdir(base):
raise OSError, 'Base is not a directory or does not exist: '+base
base_list = (os.path.abspath(base)).split(os.sep)
target_list = (os.path.abspath(target)).split(os.sep)
# On the windows platform the target may be on a completely different drive from the base.
if os.name in ['nt','dos','os2'] and base_list[0] <> target_list[0]:
raise OSError, 'Target is on a different drive to base. Target: '+target_list[0].upper()+', base: '+base_list[0].upper()
# Starting from the filepath root, work out how much of the filepath is
# shared by base and target.
for i in range(min(len(base_list), len(target_list))):
if base_list[i] <> target_list[i]: break
else:
# If we broke out of the loop, i is pointing to the first differing path elements.
# If we didn't break out of the loop, i is pointing to identical path elements.
# Increment i so that in all cases it points to the first differing path elements.
i+=1
rel_list = [os.pardir] * (len(base_list)-i) + target_list[i:]
return os.path.join(*rel_list)
--- 8< --- snip --- 8< --- snip --- 8< --- snip --- 8< ---
#!/usr/bin/python
#
# relpath_test.py
# R.Barran 30/08/2004
"""Unit test the relative path function"""
import relpath,unittest
import tempfile,os,shutil,sys
def FmtPath(testpath):
"""Format a file path in os.specific format"""
return testpath.replace('/',os.sep)
def CreateTempFile(filename):
f = open(filename,'w')
f.close()
class relpath_tests(unittest.TestCase):
def setUp(self):
"""Prepare test environment (basic setup shared by all tests)"""
# Determine where the temp directory is and run the tests there
self.TempDir = tempfile.gettempdir() + os.sep + 'relpath_tests_dir'
try:
shutil.rmtree(self.TempDir)
except:
pass
os.mkdir(self.TempDir)
os.chdir(self.TempDir)
# Create directory structure
os.makedirs('a/b/c/')
os.makedirs('a1/b1/c1/d1')
# Create a couple of files to point to
CreateTempFile('file1')
CreateTempFile('a/b/file2')
CreateTempFile('a1/b1/c1/d1/file3')
def tearDown(self):
"""Bin the temp test dir and everything in it"""
os.chdir(self.TempDir)
os.chdir(os.pardir)
shutil.rmtree('relpath_tests_dir')
#
# Checking for valid input
#
def testInvalidTargetName(self):
"""Should fail if the target does not exist """
self.assertRaises(OSError, relpath.relpath, 'a/nofilehere.txt')
def testNoTargetName(self):
"""Should fail if the target is not supplied """
self.assertRaises(OSError, relpath.relpath, '')
def testInvalidBasePath(self):
"""Should fail if the base path specified does not exist """
self.assertRaises(OSError, relpath.relpath, 'file1', 'this/path/does/not/exist')
def testBasePathIsNotADir(self):
"""Should fail if the base is anything other than a directory """
self.assertRaises(OSError, relpath.relpath, 'file1', 'a/b/file2')
def testTargetOnDifferentDrive(self):
"""On windows platform the target must be on the same drive as the base point."""
if sys.platform == 'win32':
self.assertRaises(OSError, relpath.relpath, 'z:/file99')
#
# Tests with only the target specified (no base)
#
def testTargetinCurrDir(self):
"""Target is in the current directory"""
self.assertEqual(relpath.relpath('file1'), 'file1')
self.assertEqual(relpath.relpath(os.path.abspath('file1')), 'file1')
def testTarget2DirsDown(self):
"""Target is 2 directories down"""
self.assertEqual(relpath.relpath('a/b/file2'), FmtPath('a/b/file2'))
self.assertEqual(relpath.relpath(os.path.abspath('a/b/file2')), FmtPath('a/b/file2'))
def testTarget2DirsUp(self):
"""Target is 2 directories up"""
os.chdir('a/b')
self.assertEqual(relpath.relpath('../../file1'), FmtPath('../../file1'))
self.assertEqual(relpath.relpath(os.path.abspath('../../file1')), FmtPath('../../file1'))
def testTarget2Up4Down(self):
"""Target is 2 directories up then down 4"""
os.chdir('a/b')
self.assertEqual(relpath.relpath('../../a1/b1/c1/d1/file3'), FmtPath('../../a1/b1/c1/d1/file3'))
self.assertEqual(relpath.relpath(os.path.abspath('../../a1/b1/c1/d1/file3')), FmtPath('../../a1/b1/c1/d1/file3'))
self.assertEqual(relpath.relpath(self.TempDir+'/a1/b1/c1/d1/file3'), FmtPath('../../a1/b1/c1/d1/file3'))
#
# Tests with target and base specified
#
def testTargetinCurrDir_c1(self):
"""Target is in the current directory, base is in c1"""
self.assertEqual(relpath.relpath('file1','a1/b1/c1'), FmtPath('../../../file1'))
def testTarget1DirUp_c1(self):
"""Target is 1 directory up from current dir, base is in c1"""
# Result should be same as previous test, this is just a way of checking that
# changing the curr dir has no influence on the result.
os.chdir('a')
self.assertEqual(relpath.relpath(self.TempDir+'/file1',self.TempDir+'/a1/b1/c1'), FmtPath('../../../file1'))
def testTarget2DirsDown_c1(self):
"""Target is 2 directories down from current dir, base is in c1"""
os.chdir('a')
self.assertEqual(relpath.relpath('b/file2',self.TempDir+'/a1/b1/c1'), FmtPath('../../../a/b/file2'))
#
# This final test is a bit different, it loops relpath 10000 times
# and can be used for rough performance testing of different versions of
# relpath
#
#def testSpeedTests(self):
# for i in range(10000):
# os.chdir(self.TempDir)
# self.assertEqual(relpath.relpath('file1','a1/b1/c1'), FmtPath('../../../file1'))
# self.assertEqual(relpath.relpath('a/b/file2'), FmtPath('a/b/file2'))
# os.chdir('a')
# self.assertEqual(relpath.relpath('b/file2',self.TempDir+'/a1/b1/c1'), FmtPath('../../../a/b/file2'))
if __name__ == "__main__":
unittest.main()
|
Whilst looking for a script to calculate a relative filepath, I found one on the python cookbook website. It worked, but I thought I could see ways to improve it, so I rewrote my own version.
It is different from the script on the website in that it handles only filepaths (not urls), but hopefully does a better job with improved error-handling (for example, on a windows platform it refuses to create a path to d:\adir\somefile.txt from c:\anotherdir).
I also wrote a script to automatically test relpath based on the unittest module (included above). This is the first time I write a unit test script, and it is well worth the initial effort, as it allowed me to play around with the code and rewrite it several times without breaking it.
For example, I tried replacing the for..in..else block with the following code:
i=0 while True: try: if base_list[i] <> target_list[i]: break i+=1 except: break
Performance was very similar, so I stuck with the version that showed the use of for..in..else.
An added advantage of the unit test script, that I had not heard of before, is that it allowed me to quickly check that relpath worked correctly on Win98, Win2000 and Linux.
Correct for os.path.normcase. You should take the filename case into account:
instead of: if base_list[i] You should take the filename case into account:
instead of: if base_list[i]
some assumptions are helpful, some are not. I think it is bad form to do any existance checking in this kind of routine. If you have been walking a directory structure and therefore know (as well as you can) the files exists it is just wasteful to check again.
I've written something which I think does the job and doesn't use recursion like the lisp-ish recipe also in the cookbook. See if you like some or all of this. :)
Incorrect when base is root directory:
What about:
The test case shows 2 seconds (this version) vs 2.5 (the original version, no file/directory checks).