Skip to content

Commit

Permalink
pythonGH-89812 - Add pathlib._LexicalPath
Browse files Browse the repository at this point in the history
This internal class excludes the `__fspath__()`, `__bytes__()` and
`as_uri()` methods, which must not be inherited by a future
`tarfile.TarPath` class.
  • Loading branch information
barneygale committed May 23, 2023
1 parent ae00b81 commit bddfa40
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 70 deletions.
111 changes: 60 additions & 51 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,10 @@ def __repr__(self):
return "<{}.parents>".format(type(self._path).__name__)


class PurePath(object):
"""Base class for manipulating paths without I/O.
class _LexicalPath(object):
"""Base class for manipulating paths using only lexical operations.
PurePath represents a filesystem path and offers operations which
don't imply any actual filesystem I/O. Depending on your system,
instantiating a PurePath will return either a PurePosixPath or a
PureWindowsPath object. You can also instantiate either of these classes
directly, regardless of your system.
This class does not provide __fspath__(), __bytes__() or as_uri().
"""

__slots__ = (
Expand Down Expand Up @@ -280,16 +276,6 @@ class PurePath(object):
)
_flavour = os.path

def __new__(cls, *args, **kwargs):
"""Construct a PurePath from one or several strings and or existing
PurePath objects. The strings and path objects are combined so as
to yield a canonicalized path, which is incorporated into the
new PurePath object.
"""
if cls is PurePath:
cls = PureWindowsPath if os.name == 'nt' else PurePosixPath
return object.__new__(cls)

def __reduce__(self):
# Using the parts tuple helps share interned path parts
# when pickling related paths.
Expand All @@ -298,7 +284,7 @@ def __reduce__(self):
def __init__(self, *args):
paths = []
for arg in args:
if isinstance(arg, PurePath):
if isinstance(arg, _LexicalPath):
path = arg._raw_path
else:
try:
Expand Down Expand Up @@ -378,43 +364,15 @@ def __str__(self):
self._tail) or '.'
return self._str

def __fspath__(self):
return str(self)

def as_posix(self):
"""Return the string representation of the path with forward (/)
slashes."""
f = self._flavour
return str(self).replace(f.sep, '/')

def __bytes__(self):
"""Return the bytes representation of the path. This is only
recommended to use under Unix."""
return os.fsencode(self)

def __repr__(self):
return "{}({!r})".format(self.__class__.__name__, self.as_posix())

def as_uri(self):
"""Return the path as a 'file' URI."""
if not self.is_absolute():
raise ValueError("relative path can't be expressed as a file URI")

drive = self.drive
if len(drive) == 2 and drive[1] == ':':
# It's a path on a local drive => 'file:///c:/a/b'
prefix = 'file:///' + drive
path = self.as_posix()[2:]
elif drive:
# It's a path on a network drive => 'file://host/share/a/b'
prefix = 'file:'
path = self.as_posix()
else:
# It's a posix path => 'file:///etc/hosts'
prefix = 'file://'
path = str(self)
return prefix + urlquote_from_bytes(os.fsencode(path))

@property
def _str_normcase(self):
# String with normalized case, for hashing and equality checks
Expand All @@ -434,7 +392,7 @@ def _parts_normcase(self):
return self._parts_normcase_cached

def __eq__(self, other):
if not isinstance(other, PurePath):
if not isinstance(other, _LexicalPath):
return NotImplemented
return self._str_normcase == other._str_normcase and self._flavour is other._flavour

Expand All @@ -446,22 +404,22 @@ def __hash__(self):
return self._hash

def __lt__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
if not isinstance(other, _LexicalPath) or self._flavour is not other._flavour:
return NotImplemented
return self._parts_normcase < other._parts_normcase

def __le__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
if not isinstance(other, _LexicalPath) or self._flavour is not other._flavour:
return NotImplemented
return self._parts_normcase <= other._parts_normcase

def __gt__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
if not isinstance(other, _LexicalPath) or self._flavour is not other._flavour:
return NotImplemented
return self._parts_normcase > other._parts_normcase

def __ge__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
if not isinstance(other, _LexicalPath) or self._flavour is not other._flavour:
return NotImplemented
return self._parts_normcase >= other._parts_normcase

Expand Down Expand Up @@ -707,6 +665,57 @@ def match(self, path_pattern, *, case_sensitive=None):
return False
return True


class PurePath(_LexicalPath):
"""Base class for manipulating paths without I/O.
PurePath represents a filesystem path and offers operations which
don't imply any actual filesystem I/O. Depending on your system,
instantiating a PurePath will return either a PurePosixPath or a
PureWindowsPath object. You can also instantiate either of these classes
directly, regardless of your system.
"""
__slots__ = ()

def __new__(cls, *args, **kwargs):
"""Construct a PurePath from one or several strings and or existing
PurePath objects. The strings and path objects are combined so as
to yield a canonicalized path, which is incorporated into the
new PurePath object.
"""
if cls is PurePath:
cls = PureWindowsPath if os.name == 'nt' else PurePosixPath
return object.__new__(cls)

def __fspath__(self):
return str(self)

def __bytes__(self):
"""Return the bytes representation of the path. This is only
recommended to use under Unix."""
return os.fsencode(self)

def as_uri(self):
"""Return the path as a 'file' URI."""
if not self.is_absolute():
raise ValueError("relative path can't be expressed as a file URI")

drive = self.drive
if len(drive) == 2 and drive[1] == ':':
# It's a path on a local drive => 'file:///c:/a/b'
prefix = 'file:///' + drive
path = self.as_posix()[2:]
elif drive:
# It's a path on a network drive => 'file://host/share/a/b'
prefix = 'file:'
path = self.as_posix()
else:
# It's a posix path => 'file:///etc/hosts'
prefix = 'file://'
path = str(self)
return prefix + urlquote_from_bytes(os.fsencode(path))


# Can't subclass os.PathLike from PurePath and keep the constructor
# optimizations in PurePath.__slots__.
os.PathLike.register(PurePath)
Expand Down
44 changes: 25 additions & 19 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ def with_segments(self, *pathsegments):
return type(self)(*pathsegments, session_id=self.session_id)


class _BasePurePathTest(object):
class _BaseLexicalPathTest(object):

# Keys are canonical paths, values are list of tuples of arguments
# supposed to produce equal paths.
Expand Down Expand Up @@ -227,18 +227,6 @@ def test_as_posix_common(self):
self.assertEqual(P(pathstr).as_posix(), pathstr)
# Other tests for as_posix() are in test_equivalences().

def test_as_bytes_common(self):
sep = os.fsencode(self.sep)
P = self.cls
self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b')

def test_as_uri_common(self):
P = self.cls
with self.assertRaises(ValueError):
P('a').as_uri()
with self.assertRaises(ValueError):
P().as_uri()

def test_repr_common(self):
for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'):
with self.subTest(pathstr=pathstr):
Expand Down Expand Up @@ -358,12 +346,6 @@ def test_parts_common(self):
parts = p.parts
self.assertEqual(parts, (sep, 'a', 'b'))

def test_fspath_common(self):
P = self.cls
p = P('a/b')
self._check_str(p.__fspath__(), ('a/b',))
self._check_str(os.fspath(p), ('a/b',))

def test_equivalences(self):
for k, tuples in self.equivalences.items():
canon = k.replace('/', self.sep)
Expand Down Expand Up @@ -702,6 +684,30 @@ def test_pickling_common(self):
self.assertEqual(str(pp), str(p))


class LexicalPathTest(_BaseLexicalPathTest, unittest.TestCase):
cls = pathlib._LexicalPath


class _BasePurePathTest(_BaseLexicalPathTest):
def test_fspath_common(self):
P = self.cls
p = P('a/b')
self._check_str(p.__fspath__(), ('a/b',))
self._check_str(os.fspath(p), ('a/b',))

def test_bytes_common(self):
sep = os.fsencode(self.sep)
P = self.cls
self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b')

def test_as_uri_common(self):
P = self.cls
with self.assertRaises(ValueError):
P('a').as_uri()
with self.assertRaises(ValueError):
P().as_uri()


class PurePosixPathTest(_BasePurePathTest, unittest.TestCase):
cls = pathlib.PurePosixPath

Expand Down

0 comments on commit bddfa40

Please sign in to comment.