Welcome, guest | Sign In | My Account | Store | Cart

I saw a recipe 208993 messed up with os.sep and '../' and decide to write near-pure-Python version. os.sep used in string expressions only for testing for root directory.

Function deal with Unix paths (root: "/"), Windows systems are not supported (root: "C:\").

Python, 54 lines
 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
# -*- coding: Windows-1251 -*-
'''
Relative path from one directory to another without explicit string functions (unix only)

Function deal with Unix paths (root: "/"), Windows systems are not supported (root: "C:\").
'''
__author__ = 'Denis Barmenkov <denis.barmenkov@gmail.com>'
__source__ = 'http://code.activestate.com/recipes/577356'

import os

if os.name == 'nt':
    raise ValueError, 'dos/windows paths unsupported in this version'

def relative_path(base, target):

    def split_path(path):
        res = list()
        while 1:
            path, basename = os.path.split(path) 
            if path == os.sep and basename == '':
                # root reached
                break
            res.insert(0, basename)
            if path == '':
                break
        return res

    # check for absolute paths
    if not base.startswith(os.sep):
        raise ValueError, 'base must be absolute: %s' % base
    if not target.startswith(os.sep):
        raise ValueError, 'target must be absolute: %s' % target

    base_parts = split_path(base)
    target_parts = split_path(target)

    while len(base_parts) > 0 and \
          len(target_parts) > 0 and \
          base_parts[0] == target_parts[0]:
        base_parts.pop(0)
        target_parts.pop(0)

    rel_parts = ['..'] * len(base_parts)
    rel_parts.extend(target_parts)

    return os.path.join(*rel_parts)

if __name__ == '__main__':
    base = os.sep + os.path.join('a', 'b', 'c', 'd')
    target = os.sep + os.path.join('a', 'b', 'c1', 'd2')
    print 'base  :', base
    print 'target:', target
    print 'relative base->target:', relative_path(base, target)

1 comment

Rogier Steehouder 11 years, 3 months ago  # | flag

To support windows as well:

def relative_path(base, target):
    # split off the drive
    base_drive, base = os.path.splitdrive(base)
    target_drive, target = os.path.splitdrive(target)
    # if not on the same drive, use full target path
    if base_drive != target_drive:
        return target_drive + target

    # rest of your code

    return target_drive + os.path.join(*rel_parts)

(Untested, I'm on OS X.)