[Python-checkins] GH-100502: Add `pathlib.PurePath.pathmod` attribute (GH-106533)

barneygale webhook-mailer at python.org
Wed Jul 19 14:00:00 EDT 2023


https://github.com/python/cpython/commit/c6c5665ee0c0a5ddc96da255c9a62daa332c32b3
commit: c6c5665ee0c0a5ddc96da255c9a62daa332c32b3
branch: main
author: Barney Gale <barney.gale at gmail.com>
committer: barneygale <barney.gale at gmail.com>
date: 2023-07-19T18:59:55+01:00
summary:

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

files:
A Misc/NEWS.d/next/Library/2023-07-07-21-15-17.gh-issue-100502.Iici1B.rst
M Doc/library/pathlib.rst
M Lib/pathlib.py
M Lib/test/test_pathlib.py

diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst
index af81df217eea9..01dabe286969b 100644
--- a/Doc/library/pathlib.rst
+++ b/Doc/library/pathlib.rst
@@ -303,6 +303,13 @@ Methods and properties
 
 Pure paths provide the following methods and properties:
 
+.. attribute:: PurePath.pathmod
+
+   The implementation of the :mod:`os.path` module used for low-level path
+   operations: either ``posixpath`` or ``ntpath``.
+
+   .. versionadded:: 3.13
+
 .. attribute:: PurePath.drive
 
    A string representing the drive letter or name, if any::
diff --git a/Lib/pathlib.py b/Lib/pathlib.py
index f3813e0410990..8ff4d4ea19168 100644
--- a/Lib/pathlib.py
+++ b/Lib/pathlib.py
@@ -56,8 +56,8 @@ def _ignore_error(exception):
 
 
 @functools.cache
-def _is_case_sensitive(flavour):
-    return flavour.normcase('Aa') == 'Aa'
+def _is_case_sensitive(pathmod):
+    return pathmod.normcase('Aa') == 'Aa'
 
 #
 # Globbing helpers
@@ -293,7 +293,7 @@ class PurePath:
         # path. It's set when `__hash__()` is called for the first time.
         '_hash',
     )
-    _flavour = os.path
+    pathmod = os.path
 
     def __new__(cls, *args, **kwargs):
         """Construct a PurePath from one or several strings and or existing
@@ -314,7 +314,7 @@ def __init__(self, *args):
         paths = []
         for arg in args:
             if isinstance(arg, PurePath):
-                if arg._flavour is ntpath and self._flavour is posixpath:
+                if arg.pathmod is ntpath and self.pathmod is posixpath:
                     # GH-103631: Convert separators for backwards compatibility.
                     paths.extend(path.replace('\\', '/') for path in arg._raw_paths)
                 else:
@@ -343,11 +343,11 @@ def with_segments(self, *pathsegments):
     def _parse_path(cls, path):
         if not path:
             return '', '', []
-        sep = cls._flavour.sep
-        altsep = cls._flavour.altsep
+        sep = cls.pathmod.sep
+        altsep = cls.pathmod.altsep
         if altsep:
             path = path.replace(altsep, sep)
-        drv, root, rel = cls._flavour.splitroot(path)
+        drv, root, rel = cls.pathmod.splitroot(path)
         if not root and drv.startswith(sep) and not drv.endswith(sep):
             drv_parts = drv.split(sep)
             if len(drv_parts) == 4 and drv_parts[2] not in '?.':
@@ -366,7 +366,7 @@ def _load_parts(self):
         elif len(paths) == 1:
             path = paths[0]
         else:
-            path = self._flavour.join(*paths)
+            path = self.pathmod.join(*paths)
         drv, root, tail = self._parse_path(path)
         self._drv = drv
         self._root = root
@@ -384,10 +384,10 @@ def _from_parsed_parts(self, drv, root, tail):
     @classmethod
     def _format_parsed_parts(cls, drv, root, tail):
         if drv or root:
-            return drv + root + cls._flavour.sep.join(tail)
-        elif tail and cls._flavour.splitdrive(tail[0])[0]:
+            return drv + root + cls.pathmod.sep.join(tail)
+        elif tail and cls.pathmod.splitdrive(tail[0])[0]:
             tail = ['.'] + tail
-        return cls._flavour.sep.join(tail)
+        return cls.pathmod.sep.join(tail)
 
     def __str__(self):
         """Return the string representation of the path, suitable for
@@ -405,8 +405,7 @@ def __fspath__(self):
     def as_posix(self):
         """Return the string representation of the path with forward (/)
         slashes."""
-        f = self._flavour
-        return str(self).replace(f.sep, '/')
+        return str(self).replace(self.pathmod.sep, '/')
 
     def __bytes__(self):
         """Return the bytes representation of the path.  This is only
@@ -442,7 +441,7 @@ def _str_normcase(self):
         try:
             return self._str_normcase_cached
         except AttributeError:
-            if _is_case_sensitive(self._flavour):
+            if _is_case_sensitive(self.pathmod):
                 self._str_normcase_cached = str(self)
             else:
                 self._str_normcase_cached = str(self).lower()
@@ -454,7 +453,7 @@ def _parts_normcase(self):
         try:
             return self._parts_normcase_cached
         except AttributeError:
-            self._parts_normcase_cached = self._str_normcase.split(self._flavour.sep)
+            self._parts_normcase_cached = self._str_normcase.split(self.pathmod.sep)
             return self._parts_normcase_cached
 
     @property
@@ -467,14 +466,14 @@ def _lines(self):
             if path_str == '.':
                 self._lines_cached = ''
             else:
-                trans = _SWAP_SEP_AND_NEWLINE[self._flavour.sep]
+                trans = _SWAP_SEP_AND_NEWLINE[self.pathmod.sep]
                 self._lines_cached = path_str.translate(trans)
             return self._lines_cached
 
     def __eq__(self, other):
         if not isinstance(other, PurePath):
             return NotImplemented
-        return self._str_normcase == other._str_normcase and self._flavour is other._flavour
+        return self._str_normcase == other._str_normcase and self.pathmod is other.pathmod
 
     def __hash__(self):
         try:
@@ -484,22 +483,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, PurePath) or self.pathmod is not other.pathmod:
             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, PurePath) or self.pathmod is not other.pathmod:
             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, PurePath) or self.pathmod is not other.pathmod:
             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, PurePath) or self.pathmod is not other.pathmod:
             return NotImplemented
         return self._parts_normcase >= other._parts_normcase
 
@@ -584,9 +583,9 @@ def with_name(self, name):
         """Return a new path with the file name changed."""
         if not self.name:
             raise ValueError("%r has an empty name" % (self,))
-        f = self._flavour
-        drv, root, tail = f.splitroot(name)
-        if drv or root or not tail or f.sep in tail or (f.altsep and f.altsep in tail):
+        m = self.pathmod
+        drv, root, tail = m.splitroot(name)
+        if drv or root or not tail or m.sep in tail or (m.altsep and m.altsep in tail):
             raise ValueError("Invalid name %r" % (name))
         return self._from_parsed_parts(self.drive, self.root,
                                        self._tail[:-1] + [name])
@@ -600,8 +599,8 @@ def with_suffix(self, suffix):
         has no suffix, add given suffix.  If the given suffix is an empty
         string, remove the suffix from the path.
         """
-        f = self._flavour
-        if f.sep in suffix or f.altsep and f.altsep in suffix:
+        m = self.pathmod
+        if m.sep in suffix or m.altsep and m.altsep in suffix:
             raise ValueError("Invalid suffix %r" % (suffix,))
         if suffix and not suffix.startswith('.') or suffix == '.':
             raise ValueError("Invalid suffix %r" % (suffix))
@@ -702,22 +701,22 @@ def parents(self):
     def is_absolute(self):
         """True if the path is absolute (has both a root and, if applicable,
         a drive)."""
-        if self._flavour is ntpath:
+        if self.pathmod is ntpath:
             # ntpath.isabs() is defective - see GH-44626.
             return bool(self.drive and self.root)
-        elif self._flavour is posixpath:
+        elif self.pathmod is posixpath:
             # Optimization: work with raw paths on POSIX.
             for path in self._raw_paths:
                 if path.startswith('/'):
                     return True
             return False
         else:
-            return self._flavour.isabs(str(self))
+            return self.pathmod.isabs(str(self))
 
     def is_reserved(self):
         """Return True if the path contains one of the special names reserved
         by the system, if any."""
-        if self._flavour is posixpath or not self._tail:
+        if self.pathmod is posixpath or not self._tail:
             return False
 
         # NOTE: the rules for reserved names seem somewhat complicated
@@ -737,7 +736,7 @@ def match(self, path_pattern, *, case_sensitive=None):
         if not isinstance(path_pattern, PurePath):
             path_pattern = self.with_segments(path_pattern)
         if case_sensitive is None:
-            case_sensitive = _is_case_sensitive(self._flavour)
+            case_sensitive = _is_case_sensitive(self.pathmod)
         pattern = _compile_pattern_lines(path_pattern._lines, case_sensitive)
         if path_pattern.drive or path_pattern.root:
             return pattern.match(self._lines) is not None
@@ -758,7 +757,7 @@ class PurePosixPath(PurePath):
     On a POSIX system, instantiating a PurePath should return this object.
     However, you can also instantiate it directly on any system.
     """
-    _flavour = posixpath
+    pathmod = posixpath
     __slots__ = ()
 
 
@@ -768,7 +767,7 @@ class PureWindowsPath(PurePath):
     On a Windows system, instantiating a PurePath should return this object.
     However, you can also instantiate it directly on any system.
     """
-    _flavour = ntpath
+    pathmod = ntpath
     __slots__ = ()
 
 
@@ -858,7 +857,7 @@ def is_mount(self):
         """
         Check if this path is a mount point
         """
-        return self._flavour.ismount(self)
+        return os.path.ismount(self)
 
     def is_symlink(self):
         """
@@ -879,7 +878,7 @@ def is_junction(self):
         """
         Whether this path is a junction.
         """
-        return self._flavour.isjunction(self)
+        return os.path.isjunction(self)
 
     def is_block_device(self):
         """
@@ -954,7 +953,8 @@ def samefile(self, other_path):
             other_st = other_path.stat()
         except AttributeError:
             other_st = self.with_segments(other_path).stat()
-        return self._flavour.samestat(st, other_st)
+        return (st.st_ino == other_st.st_ino and
+                st.st_dev == other_st.st_dev)
 
     def open(self, mode='r', buffering=-1, encoding=None,
              errors=None, newline=None):
@@ -1017,7 +1017,7 @@ def _scandir(self):
         return os.scandir(self)
 
     def _make_child_relpath(self, name):
-        sep = self._flavour.sep
+        sep = self.pathmod.sep
         lines_name = name.replace('\n', sep)
         lines_str = self._lines
         path_str = str(self)
@@ -1062,7 +1062,7 @@ def _glob(self, pattern, case_sensitive, follow_symlinks):
             raise ValueError("Unacceptable pattern: {!r}".format(pattern))
 
         pattern_parts = list(path_pattern._tail)
-        if pattern[-1] in (self._flavour.sep, self._flavour.altsep):
+        if pattern[-1] in (self.pathmod.sep, self.pathmod.altsep):
             # GH-65238: pathlib doesn't preserve trailing slash. Add it back.
             pattern_parts.append('')
         if pattern_parts[-1] == '**':
@@ -1071,7 +1071,7 @@ def _glob(self, pattern, case_sensitive, follow_symlinks):
 
         if case_sensitive is None:
             # TODO: evaluate case-sensitivity of each directory in _select_children().
-            case_sensitive = _is_case_sensitive(self._flavour)
+            case_sensitive = _is_case_sensitive(self.pathmod)
 
         # If symlinks are handled consistently, and the pattern does not
         # contain '..' components, then we can use a 'walk-and-match' strategy
@@ -1204,7 +1204,7 @@ def absolute(self):
             return self
         elif self.drive:
             # There is a CWD on each drive-letter drive.
-            cwd = self._flavour.abspath(self.drive)
+            cwd = os.path.abspath(self.drive)
         else:
             cwd = os.getcwd()
             # Fast path for "empty" paths, e.g. Path("."), Path("") or Path().
@@ -1230,7 +1230,7 @@ def check_eloop(e):
                 raise RuntimeError("Symlink loop from %r" % e.filename)
 
         try:
-            s = self._flavour.realpath(self, strict=strict)
+            s = os.path.realpath(self, strict=strict)
         except OSError as e:
             check_eloop(e)
             raise
@@ -1394,7 +1394,7 @@ def expanduser(self):
         """
         if (not (self.drive or self.root) and
             self._tail and self._tail[0][:1] == '~'):
-            homedir = self._flavour.expanduser(self._tail[0])
+            homedir = os.path.expanduser(self._tail[0])
             if homedir[:1] == "~":
                 raise RuntimeError("Could not determine home directory.")
             drv, root, tail = self._parse_path(homedir)
diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py
index eb2b0cfb26e85..78948e3b72032 100644
--- a/Lib/test/test_pathlib.py
+++ b/Lib/test/test_pathlib.py
@@ -66,9 +66,9 @@ class PurePathTest(unittest.TestCase):
 
     def setUp(self):
         p = self.cls('a')
-        self.flavour = p._flavour
-        self.sep = self.flavour.sep
-        self.altsep = self.flavour.altsep
+        self.pathmod = p.pathmod
+        self.sep = self.pathmod.sep
+        self.altsep = self.pathmod.altsep
 
     def test_constructor_common(self):
         P = self.cls
@@ -93,17 +93,17 @@ def test_concrete_class(self):
         p = self.cls('a')
         self.assertIs(type(p), expected)
 
-    def test_different_flavours_unequal(self):
+    def test_different_pathmods_unequal(self):
         p = self.cls('a')
-        if p._flavour is posixpath:
+        if p.pathmod is posixpath:
             q = pathlib.PureWindowsPath('a')
         else:
             q = pathlib.PurePosixPath('a')
         self.assertNotEqual(p, q)
 
-    def test_different_flavours_unordered(self):
+    def test_different_pathmods_unordered(self):
         p = self.cls('a')
-        if p._flavour is posixpath:
+        if p.pathmod is posixpath:
             q = pathlib.PureWindowsPath('a')
         else:
             q = pathlib.PurePosixPath('a')
@@ -188,16 +188,16 @@ def _get_drive_root_parts(self, parts):
         return path.drive, path.root, path.parts
 
     def _check_drive_root_parts(self, arg, *expected):
-        sep = self.flavour.sep
+        sep = self.pathmod.sep
         actual = self._get_drive_root_parts([x.replace('/', sep) for x in arg])
         self.assertEqual(actual, expected)
-        if altsep := self.flavour.altsep:
+        if altsep := self.pathmod.altsep:
             actual = self._get_drive_root_parts([x.replace('/', altsep) for x in arg])
             self.assertEqual(actual, expected)
 
     def test_drive_root_parts_common(self):
         check = self._check_drive_root_parts
-        sep = self.flavour.sep
+        sep = self.pathmod.sep
         # Unanchored parts.
         check((),                   '', '', ())
         check(('a',),               '', '', ('a',))
@@ -657,7 +657,7 @@ def test_with_suffix_common(self):
         self.assertRaises(ValueError, P('a/b').with_suffix, './.d')
         self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.')
         self.assertRaises(ValueError, P('a/b').with_suffix,
-                          (self.flavour.sep, 'd'))
+                          (self.pathmod.sep, 'd'))
 
     def test_relative_to_common(self):
         P = self.cls
@@ -2392,8 +2392,8 @@ def test_concrete_class(self):
         p = self.cls('a')
         self.assertIs(type(p), expected)
 
-    def test_unsupported_flavour(self):
-        if self.cls._flavour is os.path:
+    def test_unsupported_pathmod(self):
+        if self.cls.pathmod is os.path:
             self.skipTest("path flavour is supported")
         else:
             self.assertRaises(pathlib.UnsupportedOperation, self.cls)
@@ -2848,9 +2848,9 @@ def test_symlink_to_unsupported(self):
     def test_is_junction(self):
         P = self.cls(BASE)
 
-        with mock.patch.object(P._flavour, 'isjunction'):
-            self.assertEqual(P.is_junction(), P._flavour.isjunction.return_value)
-            P._flavour.isjunction.assert_called_once_with(P)
+        with mock.patch.object(P.pathmod, 'isjunction'):
+            self.assertEqual(P.is_junction(), P.pathmod.isjunction.return_value)
+            P.pathmod.isjunction.assert_called_once_with(P)
 
     @unittest.skipUnless(hasattr(os, "mkfifo"), "os.mkfifo() required")
     @unittest.skipIf(sys.platform == "vxworks",
diff --git a/Misc/NEWS.d/next/Library/2023-07-07-21-15-17.gh-issue-100502.Iici1B.rst b/Misc/NEWS.d/next/Library/2023-07-07-21-15-17.gh-issue-100502.Iici1B.rst
new file mode 100644
index 0000000000000..eea9564118df9
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2023-07-07-21-15-17.gh-issue-100502.Iici1B.rst
@@ -0,0 +1,3 @@
+Add :attr:`pathlib.PurePath.pathmod` class attribute that stores the
+implementation of :mod:`os.path` used for low-level path operations: either
+``posixpath`` or ``ntpath``.



More information about the Python-checkins mailing list