Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python os.path.relpath behavior

I have a directory bar inside a directory foo, with file foo_file.txt in directory foo and file bar_file.txt in directory bar; i.e.

computer$ ls
foo/
computer$ ls foo/
bar/  foo_file.txt
computer$ ls foo/bar/
bar_file.txt

Using the python os.path.relpath function, I expect:

os.path.relpath('foo/bar/bar_file.txt', 'foo/foo_file.txt')

to give me:

'bar/bar_file.txt'

However, it actually gives me:

'../bar/bar_file.txt'

Why is this? Is there an easy way to get the behavior I want?

EDIT: This is on Linux with Python 2.7.3

like image 998
jveldridge Avatar asked Jul 06 '13 19:07

jveldridge


People also ask

What does os path Relpath do?

path. relpath() method in Python is used to get a relative filepath to the given path either from the current working directory or from the given directory. Note: This method only computes the relative path.

How do you check a path is correct or not in Python?

isdir() os. path. isdir() method in Python is used to check whether the specified path is an existing directory or not.

What does os path dirname (__ file __) do?

path. dirname() method in Python is used to get the directory name from the specified path.

How do I change absolute path to relative path in Python?

First, you have to import the os module in Python so you can run operating system functionalities in your code. Then you create the variable absolute_path which fetches the current directory relative to the root folder. This is the full path to your working directory, in this case, ~/home/projects/example-project/ .


2 Answers

os.path.relpath() assumes that its arguments are directories.

>>> os.path.join(os.path.relpath(os.path.dirname('foo/bar/bar_file.txt'),
        os.path.dirname('foo/foo_file.txt')),
        os.path.basename('foo/bar/bar_file.txt'))
'bar/bar_file.txt'
like image 92
Ignacio Vazquez-Abrams Avatar answered Oct 26 '22 04:10

Ignacio Vazquez-Abrams


relpath has unexpected behavior. It treats all elements of a path as though it is a directory. So, in the path:

/path/to/a/file.txt file.txt is treated like a directory as well.

This means that when you run relpath on two paths, say,

>>> from os.path import relpath
>>> relpath('/path/to/dest/file.txt', '/path/to/origin/file.txt')
'../../dest/file.txt'

This is incorrect. The true relative path from directory origin to dest is '../dest/file.txt'

This gets especially frustrating if you're trying to create symlinks and they end up being malformed.

Solution

To fix the problem, we must first find out if the path points to a file, if not we can do the comparison as usual, otherwise we need to remove the filename from the end, do the comparison with only directories, and then add the file back to the end.

Note that this only works if you actually have these files created on your system, python must access the filesystem to find the node types.

import os

def realrelpath(origin, dest): 
    '''Get the relative path between two paths, accounting for filepaths'''

    # get the absolute paths so that strings can be compared
    origin = os.path.abspath(origin) 
    dest = os.path.abspath(dest) 

    # find out if the origin and destination are filepaths
    origin_isfile = os.path.isfile(origin)
    dest_isfile = os.path.isfile(dest)

    # if dealing with filepaths, 
    if origin_isfile or dest_isfile:
        # get the base filename
        filename = os.path.basename(origin) if origin_isfile else os.path.basename(dest)
        # in cases where we're dealing with a file, use only the directory name
        origin = os.path.dirname(origin) if origin_isfile else origin
        dest = os.path.dirname(dest) if dest_isfile else dest 
        # get the relative path between directories, then re-add the filename
        return os.path.join(os.path.relpath(dest, origin), filename)  
    else:
        # if not dealing with any filepaths, just run relpath as usual
        return os.path.relpath(dest, origin)   
                                                                                       
                                                                                                                                                           

To get the real relative path from directory origin to dest, run:

>>> relrealpath('/path/to/origin/file.txt', '/path/to/dest/file.txt')
'../dest/file.txt'

I flipped the argument order because in my brain it makes more sense to say, "I want to know the relative path to take from arg1 to get to arg2", the standard relpath implementation has it backwards (probably because that's how UNIX does it).

This need to access the filesystem is the real reason that relpath has such strange behavior. Filesystem calls are expensive, so python leaves it up to you to know whether you're dealing with a file or with a directory and only performs string operations on the path you provide.

Note: There is probably a way to make the realrelpath function a bit more efficient. For example, I'm not sure if the abspath calls are necessary, or if they could be bundled with the os.path.isfile checks with a syscall that returns more information. I welcome improvements.

like image 23
Connor Avatar answered Oct 26 '22 06:10

Connor