Skip to content

Commit c6c5665

Browse files
authored
GH-100502: Add pathlib.PurePath.pathmod attribute (GH-106533)
This instance attribute stores the implementation of `os.path` used for low-level path operations: either `posixpath` or `ntpath`.
1 parent a1a3193 commit c6c5665

File tree

4 files changed

+68
-58
lines changed

4 files changed

+68
-58
lines changed

Doc/library/pathlib.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,13 @@ Methods and properties
303303

304304
Pure paths provide the following methods and properties:
305305

306+
.. attribute:: PurePath.pathmod
307+
308+
The implementation of the :mod:`os.path` module used for low-level path
309+
operations: either ``posixpath`` or ``ntpath``.
310+
311+
.. versionadded:: 3.13
312+
306313
.. attribute:: PurePath.drive
307314

308315
A string representing the drive letter or name, if any::

Lib/pathlib.py

Lines changed: 42 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ def _ignore_error(exception):
5656

5757

5858
@functools.cache
59-
def _is_case_sensitive(flavour):
60-
return flavour.normcase('Aa') == 'Aa'
59+
def _is_case_sensitive(pathmod):
60+
return pathmod.normcase('Aa') == 'Aa'
6161

6262
#
6363
# Globbing helpers
@@ -293,7 +293,7 @@ class PurePath:
293293
# path. It's set when `__hash__()` is called for the first time.
294294
'_hash',
295295
)
296-
_flavour = os.path
296+
pathmod = os.path
297297

298298
def __new__(cls, *args, **kwargs):
299299
"""Construct a PurePath from one or several strings and or existing
@@ -314,7 +314,7 @@ def __init__(self, *args):
314314
paths = []
315315
for arg in args:
316316
if isinstance(arg, PurePath):
317-
if arg._flavour is ntpath and self._flavour is posixpath:
317+
if arg.pathmod is ntpath and self.pathmod is posixpath:
318318
# GH-103631: Convert separators for backwards compatibility.
319319
paths.extend(path.replace('\\', '/') for path in arg._raw_paths)
320320
else:
@@ -343,11 +343,11 @@ def with_segments(self, *pathsegments):
343343
def _parse_path(cls, path):
344344
if not path:
345345
return '', '', []
346-
sep = cls._flavour.sep
347-
altsep = cls._flavour.altsep
346+
sep = cls.pathmod.sep
347+
altsep = cls.pathmod.altsep
348348
if altsep:
349349
path = path.replace(altsep, sep)
350-
drv, root, rel = cls._flavour.splitroot(path)
350+
drv, root, rel = cls.pathmod.splitroot(path)
351351
if not root and drv.startswith(sep) and not drv.endswith(sep):
352352
drv_parts = drv.split(sep)
353353
if len(drv_parts) == 4 and drv_parts[2] not in '?.':
@@ -366,7 +366,7 @@ def _load_parts(self):
366366
elif len(paths) == 1:
367367
path = paths[0]
368368
else:
369-
path = self._flavour.join(*paths)
369+
path = self.pathmod.join(*paths)
370370
drv, root, tail = self._parse_path(path)
371371
self._drv = drv
372372
self._root = root
@@ -384,10 +384,10 @@ def _from_parsed_parts(self, drv, root, tail):
384384
@classmethod
385385
def _format_parsed_parts(cls, drv, root, tail):
386386
if drv or root:
387-
return drv + root + cls._flavour.sep.join(tail)
388-
elif tail and cls._flavour.splitdrive(tail[0])[0]:
387+
return drv + root + cls.pathmod.sep.join(tail)
388+
elif tail and cls.pathmod.splitdrive(tail[0])[0]:
389389
tail = ['.'] + tail
390-
return cls._flavour.sep.join(tail)
390+
return cls.pathmod.sep.join(tail)
391391

392392
def __str__(self):
393393
"""Return the string representation of the path, suitable for
@@ -405,8 +405,7 @@ def __fspath__(self):
405405
def as_posix(self):
406406
"""Return the string representation of the path with forward (/)
407407
slashes."""
408-
f = self._flavour
409-
return str(self).replace(f.sep, '/')
408+
return str(self).replace(self.pathmod.sep, '/')
410409

411410
def __bytes__(self):
412411
"""Return the bytes representation of the path. This is only
@@ -442,7 +441,7 @@ def _str_normcase(self):
442441
try:
443442
return self._str_normcase_cached
444443
except AttributeError:
445-
if _is_case_sensitive(self._flavour):
444+
if _is_case_sensitive(self.pathmod):
446445
self._str_normcase_cached = str(self)
447446
else:
448447
self._str_normcase_cached = str(self).lower()
@@ -454,7 +453,7 @@ def _parts_normcase(self):
454453
try:
455454
return self._parts_normcase_cached
456455
except AttributeError:
457-
self._parts_normcase_cached = self._str_normcase.split(self._flavour.sep)
456+
self._parts_normcase_cached = self._str_normcase.split(self.pathmod.sep)
458457
return self._parts_normcase_cached
459458

460459
@property
@@ -467,14 +466,14 @@ def _lines(self):
467466
if path_str == '.':
468467
self._lines_cached = ''
469468
else:
470-
trans = _SWAP_SEP_AND_NEWLINE[self._flavour.sep]
469+
trans = _SWAP_SEP_AND_NEWLINE[self.pathmod.sep]
471470
self._lines_cached = path_str.translate(trans)
472471
return self._lines_cached
473472

474473
def __eq__(self, other):
475474
if not isinstance(other, PurePath):
476475
return NotImplemented
477-
return self._str_normcase == other._str_normcase and self._flavour is other._flavour
476+
return self._str_normcase == other._str_normcase and self.pathmod is other.pathmod
478477

479478
def __hash__(self):
480479
try:
@@ -484,22 +483,22 @@ def __hash__(self):
484483
return self._hash
485484

486485
def __lt__(self, other):
487-
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
486+
if not isinstance(other, PurePath) or self.pathmod is not other.pathmod:
488487
return NotImplemented
489488
return self._parts_normcase < other._parts_normcase
490489

491490
def __le__(self, other):
492-
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
491+
if not isinstance(other, PurePath) or self.pathmod is not other.pathmod:
493492
return NotImplemented
494493
return self._parts_normcase <= other._parts_normcase
495494

496495
def __gt__(self, other):
497-
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
496+
if not isinstance(other, PurePath) or self.pathmod is not other.pathmod:
498497
return NotImplemented
499498
return self._parts_normcase > other._parts_normcase
500499

501500
def __ge__(self, other):
502-
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
501+
if not isinstance(other, PurePath) or self.pathmod is not other.pathmod:
503502
return NotImplemented
504503
return self._parts_normcase >= other._parts_normcase
505504

@@ -584,9 +583,9 @@ def with_name(self, name):
584583
"""Return a new path with the file name changed."""
585584
if not self.name:
586585
raise ValueError("%r has an empty name" % (self,))
587-
f = self._flavour
588-
drv, root, tail = f.splitroot(name)
589-
if drv or root or not tail or f.sep in tail or (f.altsep and f.altsep in tail):
586+
m = self.pathmod
587+
drv, root, tail = m.splitroot(name)
588+
if drv or root or not tail or m.sep in tail or (m.altsep and m.altsep in tail):
590589
raise ValueError("Invalid name %r" % (name))
591590
return self._from_parsed_parts(self.drive, self.root,
592591
self._tail[:-1] + [name])
@@ -600,8 +599,8 @@ def with_suffix(self, suffix):
600599
has no suffix, add given suffix. If the given suffix is an empty
601600
string, remove the suffix from the path.
602601
"""
603-
f = self._flavour
604-
if f.sep in suffix or f.altsep and f.altsep in suffix:
602+
m = self.pathmod
603+
if m.sep in suffix or m.altsep and m.altsep in suffix:
605604
raise ValueError("Invalid suffix %r" % (suffix,))
606605
if suffix and not suffix.startswith('.') or suffix == '.':
607606
raise ValueError("Invalid suffix %r" % (suffix))
@@ -702,22 +701,22 @@ def parents(self):
702701
def is_absolute(self):
703702
"""True if the path is absolute (has both a root and, if applicable,
704703
a drive)."""
705-
if self._flavour is ntpath:
704+
if self.pathmod is ntpath:
706705
# ntpath.isabs() is defective - see GH-44626.
707706
return bool(self.drive and self.root)
708-
elif self._flavour is posixpath:
707+
elif self.pathmod is posixpath:
709708
# Optimization: work with raw paths on POSIX.
710709
for path in self._raw_paths:
711710
if path.startswith('/'):
712711
return True
713712
return False
714713
else:
715-
return self._flavour.isabs(str(self))
714+
return self.pathmod.isabs(str(self))
716715

717716
def is_reserved(self):
718717
"""Return True if the path contains one of the special names reserved
719718
by the system, if any."""
720-
if self._flavour is posixpath or not self._tail:
719+
if self.pathmod is posixpath or not self._tail:
721720
return False
722721

723722
# NOTE: the rules for reserved names seem somewhat complicated
@@ -737,7 +736,7 @@ def match(self, path_pattern, *, case_sensitive=None):
737736
if not isinstance(path_pattern, PurePath):
738737
path_pattern = self.with_segments(path_pattern)
739738
if case_sensitive is None:
740-
case_sensitive = _is_case_sensitive(self._flavour)
739+
case_sensitive = _is_case_sensitive(self.pathmod)
741740
pattern = _compile_pattern_lines(path_pattern._lines, case_sensitive)
742741
if path_pattern.drive or path_pattern.root:
743742
return pattern.match(self._lines) is not None
@@ -758,7 +757,7 @@ class PurePosixPath(PurePath):
758757
On a POSIX system, instantiating a PurePath should return this object.
759758
However, you can also instantiate it directly on any system.
760759
"""
761-
_flavour = posixpath
760+
pathmod = posixpath
762761
__slots__ = ()
763762

764763

@@ -768,7 +767,7 @@ class PureWindowsPath(PurePath):
768767
On a Windows system, instantiating a PurePath should return this object.
769768
However, you can also instantiate it directly on any system.
770769
"""
771-
_flavour = ntpath
770+
pathmod = ntpath
772771
__slots__ = ()
773772

774773

@@ -858,7 +857,7 @@ def is_mount(self):
858857
"""
859858
Check if this path is a mount point
860859
"""
861-
return self._flavour.ismount(self)
860+
return os.path.ismount(self)
862861

863862
def is_symlink(self):
864863
"""
@@ -879,7 +878,7 @@ def is_junction(self):
879878
"""
880879
Whether this path is a junction.
881880
"""
882-
return self._flavour.isjunction(self)
881+
return os.path.isjunction(self)
883882

884883
def is_block_device(self):
885884
"""
@@ -954,7 +953,8 @@ def samefile(self, other_path):
954953
other_st = other_path.stat()
955954
except AttributeError:
956955
other_st = self.with_segments(other_path).stat()
957-
return self._flavour.samestat(st, other_st)
956+
return (st.st_ino == other_st.st_ino and
957+
st.st_dev == other_st.st_dev)
958958

959959
def open(self, mode='r', buffering=-1, encoding=None,
960960
errors=None, newline=None):
@@ -1017,7 +1017,7 @@ def _scandir(self):
10171017
return os.scandir(self)
10181018

10191019
def _make_child_relpath(self, name):
1020-
sep = self._flavour.sep
1020+
sep = self.pathmod.sep
10211021
lines_name = name.replace('\n', sep)
10221022
lines_str = self._lines
10231023
path_str = str(self)
@@ -1062,7 +1062,7 @@ def _glob(self, pattern, case_sensitive, follow_symlinks):
10621062
raise ValueError("Unacceptable pattern: {!r}".format(pattern))
10631063

10641064
pattern_parts = list(path_pattern._tail)
1065-
if pattern[-1] in (self._flavour.sep, self._flavour.altsep):
1065+
if pattern[-1] in (self.pathmod.sep, self.pathmod.altsep):
10661066
# GH-65238: pathlib doesn't preserve trailing slash. Add it back.
10671067
pattern_parts.append('')
10681068
if pattern_parts[-1] == '**':
@@ -1071,7 +1071,7 @@ def _glob(self, pattern, case_sensitive, follow_symlinks):
10711071

10721072
if case_sensitive is None:
10731073
# TODO: evaluate case-sensitivity of each directory in _select_children().
1074-
case_sensitive = _is_case_sensitive(self._flavour)
1074+
case_sensitive = _is_case_sensitive(self.pathmod)
10751075

10761076
# If symlinks are handled consistently, and the pattern does not
10771077
# contain '..' components, then we can use a 'walk-and-match' strategy
@@ -1204,7 +1204,7 @@ def absolute(self):
12041204
return self
12051205
elif self.drive:
12061206
# There is a CWD on each drive-letter drive.
1207-
cwd = self._flavour.abspath(self.drive)
1207+
cwd = os.path.abspath(self.drive)
12081208
else:
12091209
cwd = os.getcwd()
12101210
# Fast path for "empty" paths, e.g. Path("."), Path("") or Path().
@@ -1230,7 +1230,7 @@ def check_eloop(e):
12301230
raise RuntimeError("Symlink loop from %r" % e.filename)
12311231

12321232
try:
1233-
s = self._flavour.realpath(self, strict=strict)
1233+
s = os.path.realpath(self, strict=strict)
12341234
except OSError as e:
12351235
check_eloop(e)
12361236
raise
@@ -1394,7 +1394,7 @@ def expanduser(self):
13941394
"""
13951395
if (not (self.drive or self.root) and
13961396
self._tail and self._tail[0][:1] == '~'):
1397-
homedir = self._flavour.expanduser(self._tail[0])
1397+
homedir = os.path.expanduser(self._tail[0])
13981398
if homedir[:1] == "~":
13991399
raise RuntimeError("Could not determine home directory.")
14001400
drv, root, tail = self._parse_path(homedir)

0 commit comments

Comments
 (0)