11import os
22import sys
33from typing import Union
4+ from pathlib import Path
45
56_Path = Union [str , os .PathLike ]
67
@@ -11,6 +12,14 @@ def ensure_directory(path):
1112 os .makedirs (dirname , exist_ok = True )
1213
1314
15+ def safe_samefile (path1 : _Path , path2 : _Path ) -> bool :
16+ """Similar to os.path.samefile but returns False instead of raising exception"""
17+ try :
18+ return os .path .samefile (path1 , path2 )
19+ except OSError :
20+ return False
21+
22+
1423def same_path (p1 : _Path , p2 : _Path ) -> bool :
1524 """Differs from os.path.samefile because it does not require paths to exist.
1625 Purely string based (no comparison between i-nodes).
@@ -35,3 +44,21 @@ def normpath(filename: _Path) -> str:
3544 # See pkg_resources.normalize_path for notes about cygwin
3645 file = os .path .abspath (filename ) if sys .platform == 'cygwin' else filename
3746 return os .path .normcase (os .path .realpath (os .path .normpath (file )))
47+
48+
49+ def besteffort_internal_path (root : _Path , file : _Path ) -> str :
50+ """Process ``file`` and return an equivalent relative path contained into root
51+ (POSIX style, with no ``..`` segments).
52+ It may raise an ``ValueError`` if that is not possible.
53+ If ``file`` is not absolute, it will be assumed to be relative to ``root``.
54+ """
55+ path = os .path .join (root , file ) # will ignore root if file is absolute
56+ resolved = Path (path ).resolve ()
57+ logical = Path (os .path .abspath (path ))
58+
59+ # Prefer logical paths, since a parent directory can be symlinked inside root
60+ if same_path (resolved , logical ) or safe_samefile (resolved , logical ):
61+ return logical .relative_to (root ).as_posix ()
62+
63+ # Since ``resolved`` is used, it makes sense to resolve root too.
64+ return resolved .relative_to (Path (root ).resolve ()).as_posix ()
0 commit comments