Disagreement between `PureWindowsPath.relative_to()` and `ntpath.relpath()` with rootless drives
On Windows, the path C: means "the current directory on the C: drive".
But pathlib's relative_to() treats it as the immediate parent of C:\. This makes sense lexographically, but it's inconsistent with everything else:
C:\Users\Barney>python
>>> import ntpath, pathlib
>>> ntpath.relpath('C:/Windows', 'C:/Users/Barney')
'..\\..\\Windows'
>>> ntpath.relpath('C:/Windows', '.')
'..\\..\\Windows'
>>> ntpath.relpath('C:/Windows', 'C:')
'..\\..\\Windows'
>>> pathlib.Path('C:/Windows').relative_to('C:/Users/Barney', walk_up=True)
WindowsPath('../../Windows')
>>> pathlib.Path('C:/Windows').relative_to('.', walk_up=True)
ValueError: One path is relative and the other is absolute.
>>> pathlib.Path('C:/Windows').relative_to('C:')
WindowsPath('/Windows') # should be ValueError, as we're mixing absolute + relative paths
This prevents us from using relpath() from pathlib, and renders the two implementations incompatible. Booo! Also prevents us from simplifying is_relative_to() down to other == self or other in self.parents
Special cases aren't special enough to break the rules. Let's make these compatible!
Previous discussion: #84538
- PR: gh-99031
@zooba @eryksun any reason to not standardize on os.path.relpath() instead of pathlib's custom code?
Pathlib uses a pure implementation that doesn't rely on global state, such as the current working directory or current operating system (the implementation is in PurePath.relative_to). We don't resolve symlinks or junctions here, so why would we combine it with CWD?
If anything, we should make relative_to raise because C: is a relative path, and it requires an absolute path. There's a note saying that it treats a pathless root like this on purpose, but it's in a comment and not the docstring.
Provided walk_up=True doesn't affect this result, the result is correct. Anywhere on the C drive combined with \Windows will get to the right path - if it gets ..\..\Windows then it's incorrect. But I didn't try this because it's new in 3.12 and so we can change the behaviour if we feel like it.
If anything, we should make relative_to raise because C: is a relative path, and it requires an absolute path. There's a note saying that it treats a pathless root like this on purpose, but it's in a comment and not the docstring.
Indeed. This seems to be the only case where a.relative_path(b) allows a and b to have different anchors (e.g. one relative, one absolute; or different Windows drives); in all other cases, ValueError is raised. My suggestion here is that we make Path('C:/Windows').relative_to('C:') also raise.
I've logged #99199 to cover relpath() calling abspath(). It doesn't cause us any problems for us in pathlib, though (see discussion on #99031). It's "wrong" but has no observable effect, as far as I can tell.
I found another incompatibility between PurePath.relative_to() and os.path.relpath() - see https://github.com/python/cpython/issues/99199#issuecomment-1312374296
This bug is still valid, but we can't fix it by using relpath(). I've revised my PR.