Recipe2.22.Computing the Relative Path from One Directory to Another


Recipe 2.22. Computing the Relative Path from One Directory to Another

Credit: Cimarron Taylor, Alan Ezust

Problem

You need to know the relative path from one directory to anotherfor example, to create a symbolic link or a relative reference in a URL.

Solution

The simplest approach is to split paths into lists of directories, then work on the lists. Using a couple of auxiliary and somewhat generic helper functions, we could code:

import os, itertools def all_equal(elements):     ''' return True if all the elements are equal, otherwise False. '''     first_element = elements[0]     for other_element in elements[1:]:         if other_element != first_element: return False     return True def common_prefix(*sequences):     ''' return a list of common elements at the start of all sequences,         then a list of lists that are the unique tails of each sequence. '''     # if there are no sequences at all, we're done     if not sequences: return [  ], [  ]     # loop in parallel on the sequences     common = [  ]     for elements in itertools.izip(*sequences):         # unless all elements are equal, bail out of the loop         if not all_equal(elements): break         # got one more common element, append it and keep looping         common.append(elements[0])     # return the common prefix and unique tails     return common, [ sequence[len(common):] for sequence in sequences ] def relpath(p1, p2, sep=os.path.sep, pardir=os.path.pardir):     ''' return a relative path from p1 equivalent to path p2.         In particular: the empty string, if p1 == p2;                        p2, if p1 and p2 have no common prefix.     '''     common, (u1, u2) = common_prefix(p1.split(sep), p2.split(sep))     if not common:         return p2      # leave path absolute if nothing at all in common     return sep.join( [pardir]*len(u1) + u2 ) def test(p1, p2, sep=os.path.sep):     ''' call function relpath and display arguments and results. '''     print "from", p1, "to", p2, " -> ", relpath(p1, p2, sep) if _ _name_ _ == '_ _main_ _':     test('/a/b/c/d', '/a/b/c1/d1', '/')     test('/a/b/c/d', '/a/b/c/d', '/')     test('c:/x/y/z', 'd:/x/y/z', '/')

Discussion

The workhorse in this recipe is the simple but very general function common_prefix, which, given any N sequences, returns their common prefix and a list of their respective unique tails. To compute the relative path between two given paths, we can ignore their common prefix. We need only the appropriate number of move-up markers (normally, os.path.pardire.g., ../ on Unix-like systems; we need as many of them as the length of the unique tail of the starting path) followed by the unique tail of the destination path. So, function relpath splits the paths into lists of directories, calls common_prefix, and then performs exactly the construction just described.

common_prefix centers on the loop for elements in itertools.izip(*sequences), relying on the fact that izip ends with the shortest of the iterables it's zipping. The body of the loop only needs to prematurely terminate the loop as soon as it meets a tuple of elements (coming one from each sequence, per izip's specifications) that aren't all equal, and to keep track of the elements that are equal by appending one of them to list common. Once the loop is done, all that's left to prepare the lists to return is to slice off the elements that are already in common from the front of each of the sequences.

Function all_equal could alternatively be implemented in a completely different way, less simple and obvious, but interesting:

def all_equal(elements):     return len(dict.fromkeys(elements)) == 1

or, equivalently and more concisely, in Python 2.4 only,

def all_equal(elements):     return len(set(elements)) == 1

Saying that all elements are equal is exactly the same as saying that the set of the elements has cardinality (length) one. In the variation using dict.fromkeys, we use a dict to represent the set, so that variation works in Python 2.3 as well as in 2.4. The variation using set is clearer, but it only works in Python 2.4. (You could also make it work in version 2.3, as well as Python 2.4, by using the standard Python library module sets).

See Also

Library Reference and Python in a Nutshell docs for modules os and itertools.



Python Cookbook
Python Cookbook
ISBN: 0596007973
EAN: 2147483647
Year: 2004
Pages: 420

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net