mirror of
https://github.com/python/cpython.git
synced 2025-08-04 17:08:35 +00:00
GH-128520: pathlib ABCs: add JoinablePath.__vfspath__()
(#133437)
In the abstract interface of `JoinablePath`, replace `__str__()` with `__vfspath__()`. This frees user implementations of `JoinablePath` to implement `__str__()` however they like (or not at all.) Also add `pathlib._os.vfspath()`, which calls `__fspath__()` or `__vfspath__()`.
This commit is contained in:
parent
9f69a58623
commit
5dbd27db7d
8 changed files with 118 additions and 81 deletions
|
@ -28,8 +28,9 @@ except ImportError:
|
|||
|
||||
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 @@ class Path(PurePath):
|
|||
# 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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -11,9 +11,10 @@ Protocols for supporting classes in pathlib.
|
|||
|
||||
|
||||
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 @@ class PathInfo(Protocol):
|
|||
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 @@ class _JoinablePath(ABC):
|
|||
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 @@ class _JoinablePath(ABC):
|
|||
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 @@ class _JoinablePath(ABC):
|
|||
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 @@ class _JoinablePath(ABC):
|
|||
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 @@ class _JoinablePath(ABC):
|
|||
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 @@ class _JoinablePath(ABC):
|
|||
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 @@ class _WritablePath(_JoinablePath):
|
|||
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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue