Skip to content

Commit 81e301b

Browse files
committed
pythonGH-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.
1 parent ae00b81 commit 81e301b

File tree

2 files changed

+85
-70
lines changed

2 files changed

+85
-70
lines changed

Lib/pathlib.py

Lines changed: 60 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -233,14 +233,10 @@ def __repr__(self):
233233
return "<{}.parents>".format(type(self._path).__name__)
234234

235235

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

246242
__slots__ = (
@@ -280,16 +276,6 @@ class PurePath(object):
280276
)
281277
_flavour = os.path
282278

283-
def __new__(cls, *args, **kwargs):
284-
"""Construct a PurePath from one or several strings and or existing
285-
PurePath objects. The strings and path objects are combined so as
286-
to yield a canonicalized path, which is incorporated into the
287-
new PurePath object.
288-
"""
289-
if cls is PurePath:
290-
cls = PureWindowsPath if os.name == 'nt' else PurePosixPath
291-
return object.__new__(cls)
292-
293279
def __reduce__(self):
294280
# Using the parts tuple helps share interned path parts
295281
# when pickling related paths.
@@ -298,7 +284,7 @@ def __reduce__(self):
298284
def __init__(self, *args):
299285
paths = []
300286
for arg in args:
301-
if isinstance(arg, PurePath):
287+
if isinstance(arg, _LexicalPath):
302288
path = arg._raw_path
303289
else:
304290
try:
@@ -378,43 +364,15 @@ def __str__(self):
378364
self._tail) or '.'
379365
return self._str
380366

381-
def __fspath__(self):
382-
return str(self)
383-
384367
def as_posix(self):
385368
"""Return the string representation of the path with forward (/)
386369
slashes."""
387370
f = self._flavour
388371
return str(self).replace(f.sep, '/')
389372

390-
def __bytes__(self):
391-
"""Return the bytes representation of the path. This is only
392-
recommended to use under Unix."""
393-
return os.fsencode(self)
394-
395373
def __repr__(self):
396374
return "{}({!r})".format(self.__class__.__name__, self.as_posix())
397375

398-
def as_uri(self):
399-
"""Return the path as a 'file' URI."""
400-
if not self.is_absolute():
401-
raise ValueError("relative path can't be expressed as a file URI")
402-
403-
drive = self.drive
404-
if len(drive) == 2 and drive[1] == ':':
405-
# It's a path on a local drive => 'file:///c:/a/b'
406-
prefix = 'file:///' + drive
407-
path = self.as_posix()[2:]
408-
elif drive:
409-
# It's a path on a network drive => 'file://host/share/a/b'
410-
prefix = 'file:'
411-
path = self.as_posix()
412-
else:
413-
# It's a posix path => 'file:///etc/hosts'
414-
prefix = 'file://'
415-
path = str(self)
416-
return prefix + urlquote_from_bytes(os.fsencode(path))
417-
418376
@property
419377
def _str_normcase(self):
420378
# String with normalized case, for hashing and equality checks
@@ -434,7 +392,7 @@ def _parts_normcase(self):
434392
return self._parts_normcase_cached
435393

436394
def __eq__(self, other):
437-
if not isinstance(other, PurePath):
395+
if not isinstance(other, _LexicalPath):
438396
return NotImplemented
439397
return self._str_normcase == other._str_normcase and self._flavour is other._flavour
440398

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

448406
def __lt__(self, other):
449-
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
407+
if not isinstance(other, _LexicalPath) or self._flavour is not other._flavour:
450408
return NotImplemented
451409
return self._parts_normcase < other._parts_normcase
452410

453411
def __le__(self, other):
454-
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
412+
if not isinstance(other, _LexicalPath) or self._flavour is not other._flavour:
455413
return NotImplemented
456414
return self._parts_normcase <= other._parts_normcase
457415

458416
def __gt__(self, other):
459-
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
417+
if not isinstance(other, _LexicalPath) or self._flavour is not other._flavour:
460418
return NotImplemented
461419
return self._parts_normcase > other._parts_normcase
462420

463421
def __ge__(self, other):
464-
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
422+
if not isinstance(other, _LexicalPath) or self._flavour is not other._flavour:
465423
return NotImplemented
466424
return self._parts_normcase >= other._parts_normcase
467425

@@ -707,6 +665,57 @@ def match(self, path_pattern, *, case_sensitive=None):
707665
return False
708666
return True
709667

668+
669+
class PurePath(_LexicalPath):
670+
"""Base class for manipulating paths without I/O.
671+
672+
PurePath represents a filesystem path and offers operations which
673+
don't imply any actual filesystem I/O. Depending on your system,
674+
instantiating a PurePath will return either a PurePosixPath or a
675+
PureWindowsPath object. You can also instantiate either of these classes
676+
directly, regardless of your system.
677+
"""
678+
__slots__ = ()
679+
680+
def __new__(cls, *args, **kwargs):
681+
"""Construct a PurePath from one or several strings and or existing
682+
PurePath objects. The strings and path objects are combined so as
683+
to yield a canonicalized path, which is incorporated into the
684+
new PurePath object.
685+
"""
686+
if cls is PurePath:
687+
cls = PureWindowsPath if os.name == 'nt' else PurePosixPath
688+
return object.__new__(cls)
689+
690+
def __fspath__(self):
691+
return str(self)
692+
693+
def __bytes__(self):
694+
"""Return the bytes representation of the path. This is only
695+
recommended to use under Unix."""
696+
return os.fsencode(self)
697+
698+
def as_uri(self):
699+
"""Return the path as a 'file' URI."""
700+
if not self.is_absolute():
701+
raise ValueError("relative path can't be expressed as a file URI")
702+
703+
drive = self.drive
704+
if len(drive) == 2 and drive[1] == ':':
705+
# It's a path on a local drive => 'file:///c:/a/b'
706+
prefix = 'file:///' + drive
707+
path = self.as_posix()[2:]
708+
elif drive:
709+
# It's a path on a network drive => 'file://host/share/a/b'
710+
prefix = 'file:'
711+
path = self.as_posix()
712+
else:
713+
# It's a posix path => 'file:///etc/hosts'
714+
prefix = 'file://'
715+
path = str(self)
716+
return prefix + urlquote_from_bytes(os.fsencode(path))
717+
718+
710719
# Can't subclass os.PathLike from PurePath and keep the constructor
711720
# optimizations in PurePath.__slots__.
712721
os.PathLike.register(PurePath)

Lib/test/test_pathlib.py

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def with_segments(self, *pathsegments):
3737
return type(self)(*pathsegments, session_id=self.session_id)
3838

3939

40-
class _BasePurePathTest(object):
40+
class _BaseLexicalPathTest(object):
4141

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

230-
def test_as_bytes_common(self):
231-
sep = os.fsencode(self.sep)
232-
P = self.cls
233-
self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b')
234-
235-
def test_as_uri_common(self):
236-
P = self.cls
237-
with self.assertRaises(ValueError):
238-
P('a').as_uri()
239-
with self.assertRaises(ValueError):
240-
P().as_uri()
241-
242230
def test_repr_common(self):
243231
for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'):
244232
with self.subTest(pathstr=pathstr):
@@ -358,12 +346,6 @@ def test_parts_common(self):
358346
parts = p.parts
359347
self.assertEqual(parts, (sep, 'a', 'b'))
360348

361-
def test_fspath_common(self):
362-
P = self.cls
363-
p = P('a/b')
364-
self._check_str(p.__fspath__(), ('a/b',))
365-
self._check_str(os.fspath(p), ('a/b',))
366-
367349
def test_equivalences(self):
368350
for k, tuples in self.equivalences.items():
369351
canon = k.replace('/', self.sep)
@@ -702,6 +684,30 @@ def test_pickling_common(self):
702684
self.assertEqual(str(pp), str(p))
703685

704686

687+
class LexicalPathTest(_BaseLexicalPathTest, unittest.TestCase):
688+
cls = pathlib._LexicalPath
689+
690+
691+
class _BasePurePathTest(_BaseLexicalPathTest):
692+
def test_fspath_common(self):
693+
P = self.cls
694+
p = P('a/b')
695+
self._check_str(p.__fspath__(), ('a/b',))
696+
self._check_str(os.fspath(p), ('a/b',))
697+
698+
def test_bytes_common(self):
699+
sep = os.fsencode(self.sep)
700+
P = self.cls
701+
self.assertEqual(bytes(P('a/b')), b'a' + sep + b'b')
702+
703+
def test_as_uri_common(self):
704+
P = self.cls
705+
with self.assertRaises(ValueError):
706+
P('a').as_uri()
707+
with self.assertRaises(ValueError):
708+
P().as_uri()
709+
710+
705711
class PurePosixPathTest(_BasePurePathTest, unittest.TestCase):
706712
cls = pathlib.PurePosixPath
707713

0 commit comments

Comments
 (0)