10

In Python 3, I defined two paths using pathlib, say:

from pathlib import Path

origin = Path('middle-earth/gondor/minas-tirith/castle').resolve()
destination = Path('middle-earth/gondor/osgiliath/tower').resolve()

How can I get the relative path that leads from origin to destination? In this example, I'd like a function that returns ../../osgiliath/tower or something equivalent.

Ideally, I'd have a function relative_path that always satisfies

origin.joinpath(
    relative_path(origin, destination)
).resolve() == destination.resolve()

Note that Path.relative_to is insufficient in this case since origin is not a destination's parent. Also, I'm not working with symlinks, so it's safe to assume there are none if this simplifies the problem.

How can relative_path be implemented?

ruancomelli
  • 610
  • 8
  • 17
  • Replace every directory that is common for the two with `..`? As long as everything else before it is common. So IMO split by `os.sep()` or / in your case, dump each string into a list and iterate over the second until you meet an element not present in the first on the same index. Then your paths diverge. Replace every iterated element with `..`. – BoboDarph Nov 13 '19 at 14:52

2 Answers2

12

This is trivially os.path.relpath

import os.path
from pathlib import Path

origin      = Path('middle-earth/gondor/minas-tirith/castle').resolve()
destination = Path('middle-earth/gondor/osgiliath/tower').resolve()

assert os.path.relpath(destination, start=origin) == '..\\..\\osgiliath\\tower'
Adam Smith
  • 52,157
  • 12
  • 73
  • 112
  • 2
    This is great, thanks! I thought that `pathlib` would have such a seemingly simple functionality, didn't think I'd have to look anywhere else. Do you know if there's a way to do this using only `pathlib`? Anyway, your solution works beautifully. – ruancomelli Nov 13 '19 at 17:34
  • the assertion is OS dependant – Zephaniah Grunschlag Sep 05 '22 at 16:19
  • 1
    @ZephaniahGrunschlag that's true but also irrelevant in this case since the assertion is only written to demonstrate the functionality of `relpath`. – Adam Smith Sep 05 '22 at 20:52
  • Relevance is in the eye of the beholder. I didn't find it irrelevant when the example produced a misleading result on my system. – Zephaniah Grunschlag Sep 05 '22 at 22:45
2

If you'd like your own Python function to convert an absolute path to a relative path:

def absolute_file_path_to_relative(start_file_path, destination_file_path):
    return (start_file_path.count("/") + start_file_path.count("\\") + 1) * (".." + ((start_file_path.find("/") > -1) and "/" or "\\")) + destination_file_path

This assumes that:

1) start_file_path starts with the same root folder as destination_file_path.

2) Types of slashes don't occur interchangably.

3) You're not using a filesystem that permits slashes in the file name.

Those assumptions may be an advantage or disadvantage, depending on your use case.

Disadvantages: if you're using pathlib, you'll break that module's API flow in your code by mixing in this function; limited use cases; inputs have to be sterile for the filesystem you're working with.

Advantages: runs 202x faster than @AdamSmith's answer (tested on Windows 7, 32-bit)

Arundel
  • 115
  • 2
  • 2
  • 9
  • This seems to be wrong in most common cases (some permutation of `a/b/c/d, a/e/f/g` will be the most common case) and doesn't work at all for windows-style backslash-delimited paths. – Adam Smith Nov 13 '19 at 15:41
  • @AdamSmith not sure if you're talking about pathlib, but if you're using the `open` built-in function, on my Windows 7, running Python 3.7.2, it accepts both type of slashes in the shell and as stored code with absolute "C:/.." and relative "a/b/c" paths (of course using a literal string to ignore escape sequencing). – Arundel Nov 13 '19 at 16:37
  • I tested Pathlib's `Path` with the same conditions: it accepts both type of slashes in a shell session with absolute and relative paths. – Arundel Nov 13 '19 at 16:48
  • Right, but the function you wrote is using `str.count` to find how many forward slashes (`/`) there are. That won't work if the paths use backslashes to delimit paths. – Adam Smith Nov 13 '19 at 17:00
  • @AdamSmith I tried your test case and I created the folders `a/b/c/d` somewhere, and in `a` I put folders `e/f/g`. in `d` there's this script (function def was omitted here): `print(open(absolute_file_path_to_relative("a/b/c/d", "e/f/g/mytext.txt")).read())` and I get the correct contents printed of a file located in `g`. – Arundel Nov 13 '19 at 17:00
  • That's all well and good but neatly sidesteps my point which is that the most common natural inputs _will_ share a parent somewhere. `absolute_file_path_to_relative("/home/asmith/path/to/file", "/home/asmith/path/away/from/there")` will appear to succeed, but actually give a bad result. The same on Windows will also appear to succeed, but be completely wrong. – Adam Smith Nov 13 '19 at 17:03
  • Unfortunately, this solution doesn't seem to work with the arguments `origin` and `destination` defined in the question. `absolute_file_path_to_relative(str(origin), str(destination))` results in `../../../middle-earth/gondor/osgiliath/tower`, which is wrong. Correct would be at least `../../../../middle-earth/gondor/osgiliath/tower` (note the extra `../`). – ruancomelli Nov 13 '19 at 17:40
  • @AdamSmith check my edit. I don't understand what you're saying in "most common natural inputs will share a parent somewhere" with the "parent somewhere" part. Yes, the inputs can contain shared parents somewhere, but the only condition required for knowing how to arrive at the destination (apart from well-formed input) is that the inputs are absolute (as stated in the question), meaning that they share the same root folder (which is _some parent_). Once a relative path to the root parent/folder is constructed, navigating further to the destination is done by appending the destination input. – Arundel Nov 13 '19 at 18:55
  • @Arundel your edit is again brittle because ``\`` is a legal character in a *nix filesystem. My point that I seem to be failing to explain is that your code is missing the critical step of establishing that shared parent directory and stripping it from the destination path. This feels like a function `def is_prime(n): return True # precondition: n is prime` – Adam Smith Nov 13 '19 at 19:05
  • If the root parents aren't equal, then it will fail in someone else's code in the way of error "invalid path" or it may edit files you didn't intent to. Yes, that's disastrous. But there's also the argument of speed, expertise, and use case. Error checks slow down the code. If you're an "expert", you won't make that error. If your use case is the acquiring of unsanitized inputs that you didn't review yourself, your suggestion makes sense. If your use case is Windows only inputs, then the function would've been faster without checking forward slashes, etc Depending on OP, I may add to the func – Arundel Nov 13 '19 at 19:48
  • @AdamSmith I'm not familiar with nix filesystem, but my question is, how can stored Python code be standardized if backslashes are an issue?? So you just avoid backslashes in code?? – Arundel Nov 13 '19 at 19:52
  • @Arundel no, the solution cannot be Windows-specific as I'm working on a cross-platform project. The problem that Adam Smith is pointing out is that in *nix filesystems, \ is a legal character (see https://en.wikipedia.org/wiki/Filename). Therefore, your solution would fail if `origin == '/etc/My\ File\ with\ Spaces'`. Nevertheless, I liked your idea. As you can rely on the `pathlib` package, what about `def relative_path(start, dest): return Path('/'.join(['..'] * len(start.parts))) / dest`? – ruancomelli Nov 13 '19 at 22:43
  • 1
    @rugortal ah I understand. My function is definitely aimed at a limited but fast use case, so I have put out that disclaimer. – Arundel Nov 14 '19 at 16:06