Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GH-89812: Add pathlib._PurePathExt #104810

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
GH-89812: Add pathlib._LexicalPath
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
commit 81e301b876a07222283eb87de2128c0bfa5a3f80
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().
barneygale marked this conversation as resolved.
Show resolved Hide resolved
"""

__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)
merwok marked this conversation as resolved.
Show resolved Hide resolved

def __fspath__(self):
return str(self)

def __bytes__(self):
"""Return the bytes representation of the path. This is only
recommended to use under Unix."""
barneygale marked this conversation as resolved.
Show resolved Hide resolved
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__.
barneygale marked this conversation as resolved.
Show resolved Hide resolved
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