diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 338575404ff0ad..8b7425c7e70487 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 f3813e04109904..8ff4d4ea19168f 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 eb2b0cfb26e85f..78948e3b720320 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 00000000000000..eea9564118df9c --- /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``.