1
0
Fork 0

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:
Florent Rougon 2018-02-16 11:10:12 +01:00
parent dd4fc36a9d
commit 5cca99bbae
2 changed files with 214 additions and 15 deletions
scripts/python/TerraSync

View file

@ -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 doesnt 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 doesnt 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

View file

@ -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.