virtual_path.py: add more functions and properties to VirtualPath
Add more functions and properties to VirtualPath that directly correspond to functions and properties of pathlib.PurePath, except that types are adapted of course, and that for API consistency, VirtualPath methods use mixedCaseStyle whereas those of pathlib.PurePath use underscore_style.
This commit is contained in:
parent
dd4fc36a9d
commit
5cca99bbae
2 changed files with 214 additions and 15 deletions
scripts/python/TerraSync
|
@ -24,22 +24,28 @@ import pathlib
|
|||
|
||||
|
||||
class VirtualPath:
|
||||
"""Class used to represent paths inside the TerraSync repository.
|
||||
"""Class used to represent virtual paths using the slash separator.
|
||||
|
||||
This class always uses '/' as the separator. The root path '/'
|
||||
corresponds to the repository root, regardless of where it is stored
|
||||
(hard drive, remote server, etc.).
|
||||
This class always uses the slash ('/') as the separator between
|
||||
components. For terrasync.py, the root path '/' corresponds to the
|
||||
repository root, regardless of where it is stored (hard drive,
|
||||
remote server, etc.).
|
||||
|
||||
Note: because of this, the class is not supposed to be used directly
|
||||
for filesystem accesses, since some root directory or
|
||||
protocol://server/root-dir prefix would have to be prepended
|
||||
to provide reasonably useful functionality). This is why the
|
||||
class is said to be virtual. This also implies that even in
|
||||
Python 3.6 or later, the class should *not* inherit from
|
||||
os.PathLike.
|
||||
to provide reasonably useful functionality. This is why the
|
||||
paths managed by this class are said to be virtual. This also
|
||||
implies that even in Python 3.6 or later, this class should
|
||||
*not* inherit from os.PathLike.
|
||||
|
||||
Wherever a given feature exists in pathlib.PurePath, this class
|
||||
replicates the corresponding pathlib.PurePath API.
|
||||
Whenever a given feature exists in pathlib.PurePath, this class
|
||||
replicates the corresponding pathlib.PurePath API, but using
|
||||
mixedCaseStyle instead of underscore_style (the latter being used
|
||||
for every method of pathlib.PurePath). Of course, types are adapted:
|
||||
for instance, methods of this class often return a VirtualPath
|
||||
instance, whereas the corresponding pathlib.PurePath methods would
|
||||
return a pathlib.PurePath instance.
|
||||
|
||||
"""
|
||||
def __init__(self, p):
|
||||
|
@ -52,8 +58,25 @@ class VirtualPath:
|
|||
self._check()
|
||||
|
||||
def __str__(self):
|
||||
"""Return a string representation of the path in self.
|
||||
|
||||
The return value:
|
||||
- always starts with a '/';
|
||||
- never ends with a '/' except if it is exactly '/' (i.e.,
|
||||
the root virtual path).
|
||||
|
||||
"""
|
||||
return self._path
|
||||
|
||||
def asPosix(self):
|
||||
"""Return a string representation of the path in self.
|
||||
|
||||
This method returns str(self), it is only present for
|
||||
compatibility with pathlib.PurePath.
|
||||
|
||||
"""
|
||||
return str(self)
|
||||
|
||||
def __repr__(self):
|
||||
return "{}.{}({!r})".format(__name__, type(self).__name__, self._path)
|
||||
|
||||
|
@ -311,6 +334,25 @@ class VirtualPath:
|
|||
|
||||
return l
|
||||
|
||||
@property
|
||||
def stem(self):
|
||||
"""The final path component, without its suffix.
|
||||
|
||||
>>> VirtualPath('/my/library.tar.gz').stem
|
||||
'library.tar'
|
||||
>>> VirtualPath('/my/library.tar').stem
|
||||
'library'
|
||||
>>> VirtualPath('/my/library').stem
|
||||
'library'
|
||||
>>> VirtualPath('/').stem
|
||||
''
|
||||
|
||||
"""
|
||||
name = self.name
|
||||
pos = name.rfind('.')
|
||||
|
||||
return name if pos == -1 else name[:pos]
|
||||
|
||||
def asRelative(self):
|
||||
"""Return the virtual path without its leading '/'.
|
||||
|
||||
|
@ -327,8 +369,90 @@ class VirtualPath:
|
|||
assert self._path.startswith('/'), repr(self._path)
|
||||
return self._path[1:]
|
||||
|
||||
def relativeTo(self, other):
|
||||
"""Return the portion of this path that follows 'other'.
|
||||
|
||||
The return value is a string. If the operation is impossible,
|
||||
ValueError is raised.
|
||||
|
||||
>>> VirtualPath('/etc/passwd').relativeTo('/')
|
||||
'etc/passwd'
|
||||
>>> VirtualPath('/etc/passwd').relativeTo('/etc')
|
||||
'passwd'
|
||||
|
||||
"""
|
||||
normedOther = self.normalizeStringPath(other)
|
||||
|
||||
if normedOther == '/':
|
||||
return self._path[1:]
|
||||
elif self._path.startswith(normedOther):
|
||||
rest = self._path[len(normedOther):]
|
||||
|
||||
if rest.startswith('/'):
|
||||
return rest[1:]
|
||||
|
||||
raise ValueError("{!r} does not start with '{}'".format(self, other))
|
||||
|
||||
def withName(self, newName):
|
||||
"""Return a new VirtualPath instance with the 'name' part changed.
|
||||
|
||||
If the original path is '/' (which doesn’t have a name in the
|
||||
sense of the 'name' property), ValueError is raised.
|
||||
|
||||
>>> p = VirtualPath('/foobar/downloads/pathlib.tar.gz')
|
||||
>>> p.withName('setup.py')
|
||||
terrasync.virtual_path.VirtualPath('/foobar/downloads/setup.py')
|
||||
|
||||
"""
|
||||
if self._path == '/':
|
||||
raise ValueError("{!r} has an empty name".format(self))
|
||||
else:
|
||||
pos = self._path.rfind('/')
|
||||
assert pos != -1, (pos, self._path)
|
||||
|
||||
if newName.startswith('/'):
|
||||
raise ValueError("{!r} starts with a '/'".format(newName))
|
||||
elif newName.endswith('/'):
|
||||
raise ValueError("{!r} ends with a '/'".format(newName))
|
||||
else:
|
||||
return VirtualPath(self._path[:pos]) / newName
|
||||
|
||||
|
||||
def withSuffix(self, newSuffix):
|
||||
"""Return a new VirtualPath instance with the suffix changed.
|
||||
|
||||
If the original path doesn’t have a suffix, the new suffix is
|
||||
appended:
|
||||
|
||||
>>> p = VirtualPath('/foobar/downloads/pathlib.tar.gz')
|
||||
>>> p.withSuffix('.bz2')
|
||||
terrasync.virtual_path.VirtualPath('/foobar/downloads/pathlib.tar.bz2')
|
||||
>>> p = VirtualPath('/foobar/README')
|
||||
>>> p.withSuffix('.txt')
|
||||
terrasync.virtual_path.VirtualPath('/foobar/README.txt')
|
||||
|
||||
If 'self' is the root virtual path ('/') or 'newSuffix' doesn't
|
||||
start with '.', ValueError is raised.
|
||||
|
||||
"""
|
||||
if not newSuffix.startswith('.'):
|
||||
raise ValueError("new suffix {!r} doesn't start with '.'"
|
||||
.format(newSuffix))
|
||||
|
||||
name = self.name
|
||||
if not name:
|
||||
raise ValueError("{!r} has an empty 'name' part".format(self))
|
||||
|
||||
pos = name.rfind('.')
|
||||
|
||||
if pos == -1:
|
||||
return self.withName(name + newSuffix) # append suffix
|
||||
else:
|
||||
return self.withName(name[:pos] + newSuffix) # replace suffix
|
||||
|
||||
|
||||
class MutableVirtualPath(VirtualPath):
|
||||
|
||||
"""Mutable subclass of VirtualPath.
|
||||
|
||||
Contrary to VirtualPath objects, instances of this class can be
|
||||
|
|
|
@ -64,9 +64,9 @@ class VirtualPathCommonTests:
|
|||
"/abc/def")
|
||||
|
||||
# Unless the implementation of VirtualPath.__init__() has changed
|
||||
# meanwhile, test_creation() must be essentially the same as
|
||||
# meanwhile, the following function must be essentially the same as
|
||||
# test_normalizeStringPath().
|
||||
def test_creation(self):
|
||||
def test_constructor_and_str(self):
|
||||
p = self.cls("/")
|
||||
self.assertEqual(str(p), "/")
|
||||
|
||||
|
@ -91,6 +91,14 @@ class VirtualPathCommonTests:
|
|||
p = self.cls("/abc//def")
|
||||
self.assertEqual(str(p), "/abc/def")
|
||||
|
||||
def test_asPosix (self):
|
||||
self.assertEqual(self.cls("").asPosix(), "/")
|
||||
self.assertEqual(self.cls("/").asPosix(), "/")
|
||||
self.assertEqual(self.cls("/abc//def").asPosix(), "/abc/def")
|
||||
self.assertEqual(self.cls("/abc//def/").asPosix(), "/abc/def")
|
||||
self.assertEqual(self.cls("//abc//def//").asPosix(), "/abc/def")
|
||||
self.assertEqual(self.cls("////abc//def//").asPosix(), "/abc/def")
|
||||
|
||||
def test_samePath(self):
|
||||
self.assertTrue(self.cls("").samePath(self.cls("")))
|
||||
self.assertTrue(self.cls("").samePath(self.cls("/")))
|
||||
|
@ -104,9 +112,6 @@ class VirtualPathCommonTests:
|
|||
self.assertTrue(
|
||||
self.cls("/abc/def/").samePath(self.cls("/abc/def")))
|
||||
|
||||
def test_str(self):
|
||||
self.assertEqual(str(self.cls("/abc//def")), "/abc/def")
|
||||
|
||||
def test_comparisons(self):
|
||||
self.assertEqual(self.cls("/abc/def"), self.cls("/abc/def"))
|
||||
self.assertEqual(self.cls("/abc//def"), self.cls("/abc/def"))
|
||||
|
@ -207,11 +212,81 @@ class VirtualPathCommonTests:
|
|||
p = self.cls("/foo/bar/baz")
|
||||
self.assertEqual(p.suffixes, [])
|
||||
|
||||
def test_stemAttribute(self):
|
||||
p = self.cls("/")
|
||||
self.assertEqual(p.stem, '')
|
||||
|
||||
p = self.cls("/foo/bar/baz.py")
|
||||
self.assertEqual(p.stem, 'baz')
|
||||
|
||||
p = self.cls("/foo/bar/baz.py.bla")
|
||||
self.assertEqual(p.stem, 'baz.py')
|
||||
|
||||
def test_asRelative(self):
|
||||
self.assertEqual(self.cls("/").asRelative(), "")
|
||||
self.assertEqual(self.cls("/foo/bar/baz/quux/zoot").asRelative(),
|
||||
"foo/bar/baz/quux/zoot")
|
||||
|
||||
def test_relativeTo(self):
|
||||
self.assertEqual(self.cls("").relativeTo(""), "")
|
||||
self.assertEqual(self.cls("").relativeTo("/"), "")
|
||||
self.assertEqual(self.cls("/").relativeTo("/"), "")
|
||||
self.assertEqual(self.cls("/").relativeTo(""), "")
|
||||
|
||||
p = self.cls("/foo/bar/baz/quux/zoot")
|
||||
|
||||
self.assertEqual(p.relativeTo(""), "foo/bar/baz/quux/zoot")
|
||||
self.assertEqual(p.relativeTo("/"), "foo/bar/baz/quux/zoot")
|
||||
|
||||
self.assertEqual(p.relativeTo("foo"), "bar/baz/quux/zoot")
|
||||
self.assertEqual(p.relativeTo("foo/"), "bar/baz/quux/zoot")
|
||||
self.assertEqual(p.relativeTo("/foo"), "bar/baz/quux/zoot")
|
||||
self.assertEqual(p.relativeTo("/foo/"), "bar/baz/quux/zoot")
|
||||
|
||||
self.assertEqual(p.relativeTo("foo/bar/baz"), "quux/zoot")
|
||||
self.assertEqual(p.relativeTo("foo/bar/baz/"), "quux/zoot")
|
||||
self.assertEqual(p.relativeTo("/foo/bar/baz"), "quux/zoot")
|
||||
self.assertEqual(p.relativeTo("/foo/bar/baz/"), "quux/zoot")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
p.relativeTo("/foo/ba")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
p.relativeTo("/foo/balloon")
|
||||
|
||||
def test_withName(self):
|
||||
p = self.cls("/foo/bar/baz/quux/zoot")
|
||||
|
||||
self.assertEqual(p.withName(""),
|
||||
VirtualPath("/foo/bar/baz/quux"))
|
||||
self.assertEqual(p.withName("pouet"),
|
||||
VirtualPath("/foo/bar/baz/quux/pouet"))
|
||||
self.assertEqual(p.withName("pouet/zdong"),
|
||||
VirtualPath("/foo/bar/baz/quux/pouet/zdong"))
|
||||
|
||||
# The self.cls object has no 'name' (referring to the 'name' property)
|
||||
with self.assertRaises(ValueError):
|
||||
self.cls("").withName("foobar")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
self.cls("/").withName("foobar")
|
||||
|
||||
def test_withSuffix(self):
|
||||
p = self.cls("/foo/bar/baz.tar.gz")
|
||||
self.assertEqual(p.withSuffix(".bz2"),
|
||||
VirtualPath("/foo/bar/baz.tar.bz2"))
|
||||
p = self.cls("/foo/bar/baz")
|
||||
self.assertEqual(p.withSuffix(".tar.xz"),
|
||||
VirtualPath("/foo/bar/baz.tar.xz"))
|
||||
|
||||
# The self.cls object has no 'name' (referring to the 'name' property)
|
||||
with self.assertRaises(ValueError):
|
||||
self.cls("/foo/bar/baz.tar.gz").withSuffix("no-leading-dot")
|
||||
|
||||
with self.assertRaises(ValueError):
|
||||
# The root virtual path ('/') can't be used for this
|
||||
self.cls("/").withSuffix(".foobar")
|
||||
|
||||
|
||||
class TestVirtualPath(unittest.TestCase, VirtualPathCommonTests):
|
||||
"""Tests for the VirtualPath class.
|
||||
|
|
Loading…
Add table
Reference in a new issue