diff --git a/Lib/glob.py b/Lib/glob.py index 341524282ba675..1e48fe43167200 100644 --- a/Lib/glob.py +++ b/Lib/glob.py @@ -358,6 +358,12 @@ def concat_path(path, text): """ raise NotImplementedError + @staticmethod + def stringify_path(path): + """Converts the path to a string object + """ + raise NotImplementedError + # High-level methods def compile(self, pat, altsep=None): @@ -466,8 +472,9 @@ def recursive_selector(self, part, parts): select_next = self.selector(parts) def select_recursive(path, exists=False): - match_pos = len(str(path)) - if match is None or match(str(path), match_pos): + path_str = self.stringify_path(path) + match_pos = len(path_str) + if match is None or match(path_str, match_pos): yield from select_next(path, exists) stack = [path] while stack: @@ -489,7 +496,7 @@ def select_recursive_step(stack, match_pos): pass if is_dir or not dir_only: - entry_path_str = str(entry_path) + entry_path_str = self.stringify_path(entry_path) if dir_only: entry_path = self.concat_path(entry_path, self.sep) if match is None or match(entry_path_str, match_pos): @@ -529,19 +536,6 @@ def scandir(path): entries = list(scandir_it) return ((entry, entry.name, entry.path) for entry in entries) - -class _PathGlobber(_GlobberBase): - """Provides shell-style pattern matching and globbing for pathlib paths. - """ - @staticmethod - def lexists(path): - return path.info.exists(follow_symlinks=False) - - @staticmethod - def scandir(path): - return ((child.info, child.name, child) for child in path.iterdir()) - - @staticmethod - def concat_path(path, text): - return path.with_segments(str(path) + text) + def stringify_path(path): + return path # Already a string. diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 12cf9f579cb32d..2dc1f7f7126063 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -28,8 +28,9 @@ from pathlib._os import ( PathInfo, DirEntryInfo, + magic_open, vfspath, ensure_different_files, ensure_distinct_paths, - copyfile2, copyfileobj, magic_open, copy_info, + copyfile2, copyfileobj, copy_info, ) @@ -1164,12 +1165,12 @@ def _copy_from_file(self, source, preserve_metadata=False): # os.symlink() incorrectly creates a file-symlink on Windows. Avoid # this by passing *target_is_dir* to os.symlink() on Windows. def _copy_from_symlink(self, source, preserve_metadata=False): - os.symlink(str(source.readlink()), self, source.info.is_dir()) + os.symlink(vfspath(source.readlink()), self, source.info.is_dir()) if preserve_metadata: copy_info(source.info, self, follow_symlinks=False) else: def _copy_from_symlink(self, source, preserve_metadata=False): - os.symlink(str(source.readlink()), self) + os.symlink(vfspath(source.readlink()), self) if preserve_metadata: copy_info(source.info, self, follow_symlinks=False) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index 039836941dd456..62a4adb555ea89 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -210,6 +210,26 @@ def magic_open(path, mode='r', buffering=-1, encoding=None, errors=None, raise TypeError(f"{cls.__name__} can't be opened with mode {mode!r}") +def vfspath(path): + """ + Return the string representation of a virtual path object. + """ + try: + return os.fsdecode(path) + except TypeError: + pass + + path_type = type(path) + try: + return path_type.__vfspath__(path) + except AttributeError: + if hasattr(path_type, '__vfspath__'): + raise + + raise TypeError("expected str, bytes, os.PathLike or JoinablePath " + "object, not " + path_type.__name__) + + def ensure_distinct_paths(source, target): """ Raise OSError(EINVAL) if the other path is within this path. @@ -225,8 +245,8 @@ def ensure_distinct_paths(source, target): err = OSError(EINVAL, "Source path is a parent of target path") else: return - err.filename = str(source) - err.filename2 = str(target) + err.filename = vfspath(source) + err.filename2 = vfspath(target) raise err @@ -247,8 +267,8 @@ def ensure_different_files(source, target): except (OSError, ValueError): return err = OSError(EINVAL, "Source and target are the same file") - err.filename = str(source) - err.filename2 = str(target) + err.filename = vfspath(source) + err.filename2 = vfspath(target) raise err diff --git a/Lib/pathlib/types.py b/Lib/pathlib/types.py index d8f5c34a1a7513..42b80221608bcc 100644 --- a/Lib/pathlib/types.py +++ b/Lib/pathlib/types.py @@ -11,9 +11,10 @@ from abc import ABC, abstractmethod -from glob import _PathGlobber +from glob import _GlobberBase from io import text_encoding -from pathlib._os import magic_open, ensure_distinct_paths, ensure_different_files, copyfileobj +from pathlib._os import (magic_open, vfspath, ensure_distinct_paths, + ensure_different_files, copyfileobj) from pathlib import PurePath, Path from typing import Optional, Protocol, runtime_checkable @@ -60,6 +61,25 @@ def is_file(self, *, follow_symlinks: bool = True) -> bool: ... def is_symlink(self) -> bool: ... +class _PathGlobber(_GlobberBase): + """Provides shell-style pattern matching and globbing for ReadablePath. + """ + + @staticmethod + def lexists(path): + return path.info.exists(follow_symlinks=False) + + @staticmethod + def scandir(path): + return ((child.info, child.name, child) for child in path.iterdir()) + + @staticmethod + def concat_path(path, text): + return path.with_segments(vfspath(path) + text) + + stringify_path = staticmethod(vfspath) + + class _JoinablePath(ABC): """Abstract base class for pure path objects. @@ -86,20 +106,19 @@ def with_segments(self, *pathsegments): raise NotImplementedError @abstractmethod - def __str__(self): - """Return the string representation of the path, suitable for - passing to system calls.""" + def __vfspath__(self): + """Return the string representation of the path.""" raise NotImplementedError @property def anchor(self): """The concatenation of the drive and root, or ''.""" - return _explode_path(str(self), self.parser.split)[0] + return _explode_path(vfspath(self), self.parser.split)[0] @property def name(self): """The final path component, if any.""" - return self.parser.split(str(self))[1] + return self.parser.split(vfspath(self))[1] @property def suffix(self): @@ -135,7 +154,7 @@ def with_name(self, name): split = self.parser.split if split(name)[0]: raise ValueError(f"Invalid name {name!r}") - path = str(self) + path = vfspath(self) path = path.removesuffix(split(path)[1]) + name return self.with_segments(path) @@ -168,7 +187,7 @@ def with_suffix(self, suffix): def parts(self): """An object providing sequence-like access to the components in the filesystem path.""" - anchor, parts = _explode_path(str(self), self.parser.split) + anchor, parts = _explode_path(vfspath(self), self.parser.split) if anchor: parts.append(anchor) return tuple(reversed(parts)) @@ -179,24 +198,24 @@ def joinpath(self, *pathsegments): paths) or a totally different path (if one of the arguments is anchored). """ - return self.with_segments(str(self), *pathsegments) + return self.with_segments(vfspath(self), *pathsegments) def __truediv__(self, key): try: - return self.with_segments(str(self), key) + return self.with_segments(vfspath(self), key) except TypeError: return NotImplemented def __rtruediv__(self, key): try: - return self.with_segments(key, str(self)) + return self.with_segments(key, vfspath(self)) except TypeError: return NotImplemented @property def parent(self): """The logical parent of the path.""" - path = str(self) + path = vfspath(self) parent = self.parser.split(path)[0] if path != parent: return self.with_segments(parent) @@ -206,7 +225,7 @@ def parent(self): def parents(self): """A sequence of this path's logical parents.""" split = self.parser.split - path = str(self) + path = vfspath(self) parent = split(path)[0] parents = [] while path != parent: @@ -223,7 +242,7 @@ def full_match(self, pattern): case_sensitive = self.parser.normcase('Aa') == 'Aa' globber = _PathGlobber(self.parser.sep, case_sensitive, recursive=True) match = globber.compile(pattern, altsep=self.parser.altsep) - return match(str(self)) is not None + return match(vfspath(self)) is not None class _ReadablePath(_JoinablePath): @@ -412,7 +431,7 @@ def _copy_from(self, source, follow_symlinks=True): while stack: src, dst = stack.pop() if not follow_symlinks and src.info.is_symlink(): - dst.symlink_to(str(src.readlink()), src.info.is_dir()) + dst.symlink_to(vfspath(src.readlink()), src.info.is_dir()) elif src.info.is_dir(): children = src.iterdir() dst.mkdir() diff --git a/Lib/test/test_pathlib/support/lexical_path.py b/Lib/test/test_pathlib/support/lexical_path.py index f29a521af9b013..fd7fbf283a6651 100644 --- a/Lib/test/test_pathlib/support/lexical_path.py +++ b/Lib/test/test_pathlib/support/lexical_path.py @@ -9,9 +9,10 @@ from . import is_pypi if is_pypi: - from pathlib_abc import _JoinablePath + from pathlib_abc import vfspath, _JoinablePath else: from pathlib.types import _JoinablePath + from pathlib._os import vfspath class LexicalPath(_JoinablePath): @@ -22,20 +23,20 @@ def __init__(self, *pathsegments): self._segments = pathsegments def __hash__(self): - return hash(str(self)) + return hash(vfspath(self)) def __eq__(self, other): if not isinstance(other, LexicalPath): return NotImplemented - return str(self) == str(other) + return vfspath(self) == vfspath(other) - def __str__(self): + def __vfspath__(self): if not self._segments: return '' return self.parser.join(*self._segments) def __repr__(self): - return f'{type(self).__name__}({str(self)!r})' + return f'{type(self).__name__}({vfspath(self)!r})' def with_segments(self, *pathsegments): return type(self)(*pathsegments) diff --git a/Lib/test/test_pathlib/support/local_path.py b/Lib/test/test_pathlib/support/local_path.py index d481fd45ead49f..c1423c545bfd00 100644 --- a/Lib/test/test_pathlib/support/local_path.py +++ b/Lib/test/test_pathlib/support/local_path.py @@ -97,7 +97,7 @@ class LocalPathInfo(PathInfo): __slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink') def __init__(self, path): - self._path = str(path) + self._path = os.fspath(path) self._exists = None self._is_dir = None self._is_file = None @@ -139,14 +139,12 @@ class ReadableLocalPath(_ReadablePath, LexicalPath): Simple implementation of a ReadablePath class for local filesystem paths. """ __slots__ = ('info',) + __fspath__ = LexicalPath.__vfspath__ def __init__(self, *pathsegments): super().__init__(*pathsegments) self.info = LocalPathInfo(self) - def __fspath__(self): - return str(self) - def __open_rb__(self, buffering=-1): return open(self, 'rb') @@ -163,9 +161,7 @@ class WritableLocalPath(_WritablePath, LexicalPath): """ __slots__ = () - - def __fspath__(self): - return str(self) + __fspath__ = LexicalPath.__vfspath__ def __open_wb__(self, buffering=-1): return open(self, 'wb') diff --git a/Lib/test/test_pathlib/support/zip_path.py b/Lib/test/test_pathlib/support/zip_path.py index 2905260c9dfc95..21e1d07423aff5 100644 --- a/Lib/test/test_pathlib/support/zip_path.py +++ b/Lib/test/test_pathlib/support/zip_path.py @@ -16,9 +16,10 @@ from . import is_pypi if is_pypi: - from pathlib_abc import PathInfo, _ReadablePath, _WritablePath + from pathlib_abc import vfspath, PathInfo, _ReadablePath, _WritablePath else: from pathlib.types import PathInfo, _ReadablePath, _WritablePath + from pathlib._os import vfspath class ZipPathGround: @@ -34,16 +35,16 @@ def teardown(self, root): root.zip_file.close() def create_file(self, path, data=b''): - path.zip_file.writestr(str(path), data) + path.zip_file.writestr(vfspath(path), data) def create_dir(self, path): - zip_info = zipfile.ZipInfo(str(path) + '/') + zip_info = zipfile.ZipInfo(vfspath(path) + '/') zip_info.external_attr |= stat.S_IFDIR << 16 zip_info.external_attr |= stat.FILE_ATTRIBUTE_DIRECTORY path.zip_file.writestr(zip_info, '') def create_symlink(self, path, target): - zip_info = zipfile.ZipInfo(str(path)) + zip_info = zipfile.ZipInfo(vfspath(path)) zip_info.external_attr = stat.S_IFLNK << 16 path.zip_file.writestr(zip_info, target.encode()) @@ -62,28 +63,28 @@ def create_hierarchy(self, p): self.create_symlink(p.joinpath('brokenLinkLoop'), 'brokenLinkLoop') def readtext(self, p): - with p.zip_file.open(str(p), 'r') as f: + with p.zip_file.open(vfspath(p), 'r') as f: f = io.TextIOWrapper(f, encoding='utf-8') return f.read() def readbytes(self, p): - with p.zip_file.open(str(p), 'r') as f: + with p.zip_file.open(vfspath(p), 'r') as f: return f.read() readlink = readtext def isdir(self, p): - path_str = str(p) + "/" + path_str = vfspath(p) + "/" return path_str in p.zip_file.NameToInfo def isfile(self, p): - info = p.zip_file.NameToInfo.get(str(p)) + info = p.zip_file.NameToInfo.get(vfspath(p)) if info is None: return False return not stat.S_ISLNK(info.external_attr >> 16) def islink(self, p): - info = p.zip_file.NameToInfo.get(str(p)) + info = p.zip_file.NameToInfo.get(vfspath(p)) if info is None: return False return stat.S_ISLNK(info.external_attr >> 16) @@ -240,20 +241,20 @@ def __init__(self, *pathsegments, zip_file): zip_file.filelist = ZipFileList(zip_file) def __hash__(self): - return hash((str(self), self.zip_file)) + return hash((vfspath(self), self.zip_file)) def __eq__(self, other): if not isinstance(other, ReadableZipPath): return NotImplemented - return str(self) == str(other) and self.zip_file is other.zip_file + return vfspath(self) == vfspath(other) and self.zip_file is other.zip_file - def __str__(self): + def __vfspath__(self): if not self._segments: return '' return self.parser.join(*self._segments) def __repr__(self): - return f'{type(self).__name__}({str(self)!r}, zip_file={self.zip_file!r})' + return f'{type(self).__name__}({vfspath(self)!r}, zip_file={self.zip_file!r})' def with_segments(self, *pathsegments): return type(self)(*pathsegments, zip_file=self.zip_file) @@ -261,7 +262,7 @@ def with_segments(self, *pathsegments): @property def info(self): tree = self.zip_file.filelist.tree - return tree.resolve(str(self), follow_symlinks=False) + return tree.resolve(vfspath(self), follow_symlinks=False) def __open_rb__(self, buffering=-1): info = self.info.resolve() @@ -301,36 +302,36 @@ def __init__(self, *pathsegments, zip_file): self.zip_file = zip_file def __hash__(self): - return hash((str(self), self.zip_file)) + return hash((vfspath(self), self.zip_file)) def __eq__(self, other): if not isinstance(other, WritableZipPath): return NotImplemented - return str(self) == str(other) and self.zip_file is other.zip_file + return vfspath(self) == vfspath(other) and self.zip_file is other.zip_file - def __str__(self): + def __vfspath__(self): if not self._segments: return '' return self.parser.join(*self._segments) def __repr__(self): - return f'{type(self).__name__}({str(self)!r}, zip_file={self.zip_file!r})' + return f'{type(self).__name__}({vfspath(self)!r}, zip_file={self.zip_file!r})' def with_segments(self, *pathsegments): return type(self)(*pathsegments, zip_file=self.zip_file) def __open_wb__(self, buffering=-1): - return self.zip_file.open(str(self), 'w') + return self.zip_file.open(vfspath(self), 'w') def mkdir(self, mode=0o777): - zinfo = zipfile.ZipInfo(str(self) + '/') + zinfo = zipfile.ZipInfo(vfspath(self) + '/') zinfo.external_attr |= stat.S_IFDIR << 16 zinfo.external_attr |= stat.FILE_ATTRIBUTE_DIRECTORY self.zip_file.writestr(zinfo, '') def symlink_to(self, target, target_is_directory=False): - zinfo = zipfile.ZipInfo(str(self)) + zinfo = zipfile.ZipInfo(vfspath(self)) zinfo.external_attr = stat.S_IFLNK << 16 if target_is_directory: zinfo.external_attr |= 0x10 - self.zip_file.writestr(zinfo, str(target)) + self.zip_file.writestr(zinfo, vfspath(target)) diff --git a/Lib/test/test_pathlib/test_join_windows.py b/Lib/test/test_pathlib/test_join_windows.py index 2cc634f25efc68..f30c80605f7f91 100644 --- a/Lib/test/test_pathlib/test_join_windows.py +++ b/Lib/test/test_pathlib/test_join_windows.py @@ -8,6 +8,11 @@ from .support import is_pypi from .support.lexical_path import LexicalWindowsPath +if is_pypi: + from pathlib_abc import vfspath +else: + from pathlib._os import vfspath + class JoinTestBase: def test_join(self): @@ -70,17 +75,17 @@ def test_div(self): self.assertEqual(p / './dd:s', P(r'C:/a/b\./dd:s')) self.assertEqual(p / 'E:d:s', P('E:d:s')) - def test_str(self): + def test_vfspath(self): p = self.cls(r'a\b\c') - self.assertEqual(str(p), 'a\\b\\c') + self.assertEqual(vfspath(p), 'a\\b\\c') p = self.cls(r'c:\a\b\c') - self.assertEqual(str(p), 'c:\\a\\b\\c') + self.assertEqual(vfspath(p), 'c:\\a\\b\\c') p = self.cls('\\\\a\\b\\') - self.assertEqual(str(p), '\\\\a\\b\\') + self.assertEqual(vfspath(p), '\\\\a\\b\\') p = self.cls(r'\\a\b\c') - self.assertEqual(str(p), '\\\\a\\b\\c') + self.assertEqual(vfspath(p), '\\\\a\\b\\c') p = self.cls(r'\\a\b\c\d') - self.assertEqual(str(p), '\\\\a\\b\\c\\d') + self.assertEqual(vfspath(p), '\\\\a\\b\\c\\d') def test_parts(self): P = self.cls