4848
4949WIN32 = sys .platform .startswith ('win32' )
5050
51+ PYTHON_LT_3_8 = sys .version_info [0 :2 ] < (3 , 8 )
52+ PYTHON_GE_3_12 = sys .version_info [0 :2 ] >= (3 , 12 )
53+
5154
5255def augpath (path , suffix = '' , prefix = '' , ext = None , tail = '' , base = None ,
5356 dpath = None , relative = None , multidot = False ):
@@ -1228,7 +1231,57 @@ def touch(self, mode=0o0666, exist_ok=True):
12281231 super ().touch (mode = mode , exist_ok = exist_ok )
12291232 return self
12301233
1231- def walk (self , topdown = True , onerror = None , followlinks = False ):
1234+ def relative_to (self , * other , ** kwargs ):
1235+ """
1236+ Return the relative path to another path identified by the passed
1237+ arguments. If the operation is not possible (because this is not a
1238+ subpath of the other path), raise ValueError.
1239+
1240+ Includes a Backport of :meth:`pathlib.Path.relative_to` with
1241+ ``walk_up=True`` that's not available pre 3.12.
1242+
1243+ Args:
1244+ other (Path | str): the base path
1245+
1246+ walk_up (bool):
1247+ controls whether `..` may be used to resolve the path.
1248+
1249+ Returns:
1250+ Path: the new relative path
1251+
1252+ References:
1253+ https://stackoverflow.com/questions/38083555/using-pathlibs-relative-to-for-directories-on-the-same-level
1254+ https://github.com/p2p-ld/numpydantic/blob/66fffc49f87bfaaa2f4d05bf1730c343b10c9cc6/src/numpydantic/serialization.py#L107-L142
1255+
1256+ Example:
1257+ >>> import ubelt as ub
1258+ >>> import pytest
1259+ >>> self = ub.Path('foo/bar')
1260+ >>> other = ub.Path('foo/bar/baz')
1261+ >>> result = self.relative_to(other, walk_up=True)
1262+ >>> assert result == ub.Path('..')
1263+ >>> with pytest.raises(ValueError):
1264+ >>> self.relative_to(other)
1265+ >>> with pytest.raises(ValueError):
1266+ >>> self.relative_to(other, walk_up=False)
1267+ >>> with pytest.raises(TypeError):
1268+ >>> self.relative_to(other, not_a_kwarg=False)
1269+ """
1270+ if PYTHON_GE_3_12 :
1271+ return super ().relative_to (* other , ** kwargs )
1272+ else :
1273+ # Test to see if we need the backport
1274+ walk_up = kwargs .pop ('walk_up' , False )
1275+ if len (kwargs ):
1276+ bad_key = list (kwargs )[0 ]
1277+ raise TypeError (f'{ self .__class__ .__name__ } .relative_to() got an unexpected keyword argument { bad_key !r} ' )
1278+ if not walk_up :
1279+ return super ().relative_to (* other , ** kwargs )
1280+ else :
1281+ # Use the backport
1282+ return _relative_path_backport (self , other , walk_up = walk_up )
1283+
1284+ def walk (self , topdown = True , onerror = None , followlinks = False , ** kwargs ):
12321285 """
12331286 A variant of :func:`os.walk` for pathlib
12341287
@@ -1244,6 +1297,11 @@ def walk(self, topdown=True, onerror=None, followlinks=False):
12441297 followlinks (bool):
12451298 if True recurse into symbolic directory links
12461299
1300+ **kwargs:
1301+ Accepts aliases the 3.12 version of the above names: top_down,
1302+ on_error, follow_symlinks. In the future we may switch the 3.12
1303+ variants to be the primary arguments.
1304+
12471305 Yields:
12481306 Tuple['Path', List[str], List[str]]:
12491307 the root path, directory names, and file names
@@ -1274,11 +1332,28 @@ def walk(self, topdown=True, onerror=None, followlinks=False):
12741332 >>> if 'CVS' in dirs:
12751333 >>> dirs.remove('CVS') # don't visit CVS directories
12761334 """
1277- cls = self .__class__
1278- walker = os .walk (self , topdown = topdown , onerror = onerror ,
1279- followlinks = followlinks )
1280- for root , dnames , fnames in walker :
1281- yield (cls (root ), dnames , fnames )
1335+ # Add kwargs to support ubelt original kwargs as well as pathlib kwargs
1336+ top_down = kwargs .pop ('top_down' , topdown )
1337+ on_error = kwargs .pop ('on_error' , onerror )
1338+ follow_symlinks = kwargs .pop ('follow_symlinks' , followlinks )
1339+
1340+ if len (kwargs ):
1341+ bad_key = list (kwargs )[0 ]
1342+ raise TypeError (f'{ self .__class__ .__name__ } .relative_to() got an unexpected keyword argument { bad_key !r} ' )
1343+
1344+ if PYTHON_GE_3_12 :
1345+ # Use the parent implementation if available
1346+ yield from super ().walk (
1347+ top_down = top_down , on_error = on_error ,
1348+ follow_symlinks = follow_symlinks )
1349+ else :
1350+ # TODO: backport the 3.12 implementation, which is more efficient
1351+ # Our original implementation
1352+ cls = self .__class__
1353+ walker = os .walk (self , topdown = top_down , onerror = on_error ,
1354+ followlinks = follow_symlinks )
1355+ for root , dnames , fnames in walker :
1356+ yield (cls (root ), dnames , fnames )
12821357
12831358 def __add__ (self , other ):
12841359 """
@@ -1565,7 +1640,7 @@ def copy(self, dst, follow_file_symlinks=False, follow_dir_symlinks=False,
15651640 _patch_win32_stats_on_pypy ()
15661641
15671642 if self .is_dir ():
1568- if sys . version_info [ 0 : 2 ] < ( 3 , 8 ) : # nocover
1643+ if PYTHON_LT_3_8 : # nocover
15691644 copytree = _compat_copytree
15701645 else :
15711646 copytree = shutil .copytree
@@ -1894,7 +1969,35 @@ def _patch_win32_stats_on_pypy():
18941969 stat .IO_REPARSE_TAG_SYMLINK = 0xa000000c # windows
18951970
18961971
1897- if sys .version_info [0 :2 ] < (3 , 8 ): # nocover
1972+ def _relative_path_backport (self , other , walk_up = False ):
1973+ if not isinstance (other , _PathBase ):
1974+ other = type (self )(* other )
1975+ # other = self.with_segments(other)
1976+ # anchor0, parts0 = self._stack
1977+ # anchor1, parts1 = other._stack
1978+ self_parts = self .parts
1979+ other_parts = other .parts
1980+ anchor0 , parts0 = self_parts [0 ], list (reversed (self_parts [1 :]))
1981+ anchor1 , parts1 = other_parts [0 ], list (reversed (other_parts [1 :]))
1982+ if anchor0 != anchor1 :
1983+ raise ValueError (f"{ self ._raw_path !r} and { other ._raw_path !r} have different anchors" )
1984+ while parts0 and parts1 and parts0 [- 1 ] == parts1 [- 1 ]:
1985+ parts0 .pop ()
1986+ parts1 .pop ()
1987+ for part in parts1 :
1988+ if not part or part == '.' :
1989+ pass
1990+ elif not walk_up :
1991+ raise ValueError (f"{ self ._raw_path !r} is not in the subpath of { other ._raw_path !r} " )
1992+ elif part == '..' :
1993+ raise ValueError (f"'..' segment in { other ._raw_path !r} cannot be walked" )
1994+ else :
1995+ parts0 .append ('..' )
1996+ # return self.with_segments('', *reversed(parts0))
1997+ return type (self )('' , * reversed (parts0 ))
1998+
1999+
2000+ if PYTHON_LT_3_8 : # nocover
18982001
18992002 # Vendor in a nearly modern copytree for Python 3.6 and 3.7
19002003 def _compat_copytree (src , dst , symlinks = False , ignore = None ,
0 commit comments