mirror of
https://github.com/python/cpython.git
synced 2025-08-02 16:13:13 +00:00
gh-68320, gh-88302 - Allow for private pathlib.Path
subclassing (GH-31691)
Users may wish to define subclasses of `pathlib.Path` to add or modify existing methods. Before this change, attempting to instantiate a subclass raised an exception like: AttributeError: type object 'PPath' has no attribute '_flavour' Previously the `_flavour` attribute was assigned as follows: PurePath._flavour = xxx not set!! xxx PurePosixPath._flavour = _PosixFlavour() PureWindowsPath._flavour = _WindowsFlavour() This change replaces it with a `_pathmod` attribute, set as follows: PurePath._pathmod = os.path PurePosixPath._pathmod = posixpath PureWindowsPath._pathmod = ntpath Functionality from `_PosixFlavour` and `_WindowsFlavour` is moved into `PurePath` as underscored-prefixed classmethods. Flavours are removed. Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com> Co-authored-by: Brett Cannon <brett@python.org> Co-authored-by: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Co-authored-by: Eryk Sun <eryksun@gmail.com>
This commit is contained in:
parent
5d84966cce
commit
a68e585c8b
3 changed files with 179 additions and 232 deletions
331
Lib/pathlib.py
331
Lib/pathlib.py
|
@ -30,6 +30,14 @@ __all__ = [
|
||||||
# Internals
|
# Internals
|
||||||
#
|
#
|
||||||
|
|
||||||
|
# Reference for Windows paths can be found at
|
||||||
|
# https://learn.microsoft.com/en-gb/windows/win32/fileio/naming-a-file .
|
||||||
|
_WIN_RESERVED_NAMES = frozenset(
|
||||||
|
{'CON', 'PRN', 'AUX', 'NUL', 'CONIN$', 'CONOUT$'} |
|
||||||
|
{f'COM{c}' for c in '123456789\xb9\xb2\xb3'} |
|
||||||
|
{f'LPT{c}' for c in '123456789\xb9\xb2\xb3'}
|
||||||
|
)
|
||||||
|
|
||||||
_WINERROR_NOT_READY = 21 # drive exists but is not accessible
|
_WINERROR_NOT_READY = 21 # drive exists but is not accessible
|
||||||
_WINERROR_INVALID_NAME = 123 # fix for bpo-35306
|
_WINERROR_INVALID_NAME = 123 # fix for bpo-35306
|
||||||
_WINERROR_CANT_RESOLVE_FILENAME = 1921 # broken symlink pointing to itself
|
_WINERROR_CANT_RESOLVE_FILENAME = 1921 # broken symlink pointing to itself
|
||||||
|
@ -52,150 +60,6 @@ def _is_wildcard_pattern(pat):
|
||||||
# be looked up directly as a file.
|
# be looked up directly as a file.
|
||||||
return "*" in pat or "?" in pat or "[" in pat
|
return "*" in pat or "?" in pat or "[" in pat
|
||||||
|
|
||||||
|
|
||||||
class _Flavour(object):
|
|
||||||
"""A flavour implements a particular (platform-specific) set of path
|
|
||||||
semantics."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.join = self.sep.join
|
|
||||||
|
|
||||||
def parse_parts(self, parts):
|
|
||||||
if not parts:
|
|
||||||
return '', '', []
|
|
||||||
sep = self.sep
|
|
||||||
altsep = self.altsep
|
|
||||||
path = self.pathmod.join(*parts)
|
|
||||||
if altsep:
|
|
||||||
path = path.replace(altsep, sep)
|
|
||||||
drv, root, rel = self.splitroot(path)
|
|
||||||
unfiltered_parsed = [drv + root] + rel.split(sep)
|
|
||||||
parsed = [sys.intern(x) for x in unfiltered_parsed if x and x != '.']
|
|
||||||
return drv, root, parsed
|
|
||||||
|
|
||||||
def join_parsed_parts(self, drv, root, parts, drv2, root2, parts2):
|
|
||||||
"""
|
|
||||||
Join the two paths represented by the respective
|
|
||||||
(drive, root, parts) tuples. Return a new (drive, root, parts) tuple.
|
|
||||||
"""
|
|
||||||
if root2:
|
|
||||||
if not drv2 and drv:
|
|
||||||
return drv, root2, [drv + root2] + parts2[1:]
|
|
||||||
elif drv2:
|
|
||||||
if drv2 == drv or self.casefold(drv2) == self.casefold(drv):
|
|
||||||
# Same drive => second path is relative to the first
|
|
||||||
return drv, root, parts + parts2[1:]
|
|
||||||
else:
|
|
||||||
# Second path is non-anchored (common case)
|
|
||||||
return drv, root, parts + parts2
|
|
||||||
return drv2, root2, parts2
|
|
||||||
|
|
||||||
|
|
||||||
class _WindowsFlavour(_Flavour):
|
|
||||||
# Reference for Windows paths can be found at
|
|
||||||
# http://msdn.microsoft.com/en-us/library/aa365247%28v=vs.85%29.aspx
|
|
||||||
|
|
||||||
sep = '\\'
|
|
||||||
altsep = '/'
|
|
||||||
has_drv = True
|
|
||||||
pathmod = ntpath
|
|
||||||
|
|
||||||
is_supported = (os.name == 'nt')
|
|
||||||
|
|
||||||
reserved_names = (
|
|
||||||
{'CON', 'PRN', 'AUX', 'NUL', 'CONIN$', 'CONOUT$'} |
|
|
||||||
{'COM%s' % c for c in '123456789\xb9\xb2\xb3'} |
|
|
||||||
{'LPT%s' % c for c in '123456789\xb9\xb2\xb3'}
|
|
||||||
)
|
|
||||||
|
|
||||||
def splitroot(self, part, sep=sep):
|
|
||||||
drv, rest = self.pathmod.splitdrive(part)
|
|
||||||
if drv[:1] == sep or rest[:1] == sep:
|
|
||||||
return drv, sep, rest.lstrip(sep)
|
|
||||||
else:
|
|
||||||
return drv, '', rest
|
|
||||||
|
|
||||||
def casefold(self, s):
|
|
||||||
return s.lower()
|
|
||||||
|
|
||||||
def casefold_parts(self, parts):
|
|
||||||
return [p.lower() for p in parts]
|
|
||||||
|
|
||||||
def compile_pattern(self, pattern):
|
|
||||||
return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch
|
|
||||||
|
|
||||||
def is_reserved(self, parts):
|
|
||||||
# NOTE: the rules for reserved names seem somewhat complicated
|
|
||||||
# (e.g. r"..\NUL" is reserved but not r"foo\NUL" if "foo" does not
|
|
||||||
# exist). We err on the side of caution and return True for paths
|
|
||||||
# which are not considered reserved by Windows.
|
|
||||||
if not parts:
|
|
||||||
return False
|
|
||||||
if parts[0].startswith('\\\\'):
|
|
||||||
# UNC paths are never reserved
|
|
||||||
return False
|
|
||||||
name = parts[-1].partition('.')[0].partition(':')[0].rstrip(' ')
|
|
||||||
return name.upper() in self.reserved_names
|
|
||||||
|
|
||||||
def make_uri(self, path):
|
|
||||||
# Under Windows, file URIs use the UTF-8 encoding.
|
|
||||||
drive = path.drive
|
|
||||||
if len(drive) == 2 and drive[1] == ':':
|
|
||||||
# It's a path on a local drive => 'file:///c:/a/b'
|
|
||||||
rest = path.as_posix()[2:].lstrip('/')
|
|
||||||
return 'file:///%s/%s' % (
|
|
||||||
drive, urlquote_from_bytes(rest.encode('utf-8')))
|
|
||||||
else:
|
|
||||||
# It's a path on a network drive => 'file://host/share/a/b'
|
|
||||||
return 'file:' + urlquote_from_bytes(path.as_posix().encode('utf-8'))
|
|
||||||
|
|
||||||
|
|
||||||
class _PosixFlavour(_Flavour):
|
|
||||||
sep = '/'
|
|
||||||
altsep = ''
|
|
||||||
has_drv = False
|
|
||||||
pathmod = posixpath
|
|
||||||
|
|
||||||
is_supported = (os.name != 'nt')
|
|
||||||
|
|
||||||
def splitroot(self, part, sep=sep):
|
|
||||||
if part and part[0] == sep:
|
|
||||||
stripped_part = part.lstrip(sep)
|
|
||||||
# According to POSIX path resolution:
|
|
||||||
# http://pubs.opengroup.org/onlinepubs/009695399/basedefs/xbd_chap04.html#tag_04_11
|
|
||||||
# "A pathname that begins with two successive slashes may be
|
|
||||||
# interpreted in an implementation-defined manner, although more
|
|
||||||
# than two leading slashes shall be treated as a single slash".
|
|
||||||
if len(part) - len(stripped_part) == 2:
|
|
||||||
return '', sep * 2, stripped_part
|
|
||||||
else:
|
|
||||||
return '', sep, stripped_part
|
|
||||||
else:
|
|
||||||
return '', '', part
|
|
||||||
|
|
||||||
def casefold(self, s):
|
|
||||||
return s
|
|
||||||
|
|
||||||
def casefold_parts(self, parts):
|
|
||||||
return parts
|
|
||||||
|
|
||||||
def compile_pattern(self, pattern):
|
|
||||||
return re.compile(fnmatch.translate(pattern)).fullmatch
|
|
||||||
|
|
||||||
def is_reserved(self, parts):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def make_uri(self, path):
|
|
||||||
# We represent the path using the local filesystem encoding,
|
|
||||||
# for portability to other applications.
|
|
||||||
bpath = bytes(path)
|
|
||||||
return 'file://' + urlquote_from_bytes(bpath)
|
|
||||||
|
|
||||||
|
|
||||||
_windows_flavour = _WindowsFlavour()
|
|
||||||
_posix_flavour = _PosixFlavour()
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Globbing helpers
|
# Globbing helpers
|
||||||
#
|
#
|
||||||
|
@ -237,14 +101,15 @@ class _Selector:
|
||||||
is_dir = path_cls.is_dir
|
is_dir = path_cls.is_dir
|
||||||
exists = path_cls.exists
|
exists = path_cls.exists
|
||||||
scandir = path_cls._scandir
|
scandir = path_cls._scandir
|
||||||
|
normcase = path_cls._flavour.normcase
|
||||||
if not is_dir(parent_path):
|
if not is_dir(parent_path):
|
||||||
return iter([])
|
return iter([])
|
||||||
return self._select_from(parent_path, is_dir, exists, scandir)
|
return self._select_from(parent_path, is_dir, exists, scandir, normcase)
|
||||||
|
|
||||||
|
|
||||||
class _TerminatingSelector:
|
class _TerminatingSelector:
|
||||||
|
|
||||||
def _select_from(self, parent_path, is_dir, exists, scandir):
|
def _select_from(self, parent_path, is_dir, exists, scandir, normcase):
|
||||||
yield parent_path
|
yield parent_path
|
||||||
|
|
||||||
|
|
||||||
|
@ -254,11 +119,11 @@ class _PreciseSelector(_Selector):
|
||||||
self.name = name
|
self.name = name
|
||||||
_Selector.__init__(self, child_parts, flavour)
|
_Selector.__init__(self, child_parts, flavour)
|
||||||
|
|
||||||
def _select_from(self, parent_path, is_dir, exists, scandir):
|
def _select_from(self, parent_path, is_dir, exists, scandir, normcase):
|
||||||
try:
|
try:
|
||||||
path = parent_path._make_child_relpath(self.name)
|
path = parent_path._make_child_relpath(self.name)
|
||||||
if (is_dir if self.dironly else exists)(path):
|
if (is_dir if self.dironly else exists)(path):
|
||||||
for p in self.successor._select_from(path, is_dir, exists, scandir):
|
for p in self.successor._select_from(path, is_dir, exists, scandir, normcase):
|
||||||
yield p
|
yield p
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
return
|
return
|
||||||
|
@ -267,10 +132,10 @@ class _PreciseSelector(_Selector):
|
||||||
class _WildcardSelector(_Selector):
|
class _WildcardSelector(_Selector):
|
||||||
|
|
||||||
def __init__(self, pat, child_parts, flavour):
|
def __init__(self, pat, child_parts, flavour):
|
||||||
self.match = flavour.compile_pattern(pat)
|
self.match = re.compile(fnmatch.translate(flavour.normcase(pat))).fullmatch
|
||||||
_Selector.__init__(self, child_parts, flavour)
|
_Selector.__init__(self, child_parts, flavour)
|
||||||
|
|
||||||
def _select_from(self, parent_path, is_dir, exists, scandir):
|
def _select_from(self, parent_path, is_dir, exists, scandir, normcase):
|
||||||
try:
|
try:
|
||||||
# We must close the scandir() object before proceeding to
|
# We must close the scandir() object before proceeding to
|
||||||
# avoid exhausting file descriptors when globbing deep trees.
|
# avoid exhausting file descriptors when globbing deep trees.
|
||||||
|
@ -289,9 +154,9 @@ class _WildcardSelector(_Selector):
|
||||||
raise
|
raise
|
||||||
continue
|
continue
|
||||||
name = entry.name
|
name = entry.name
|
||||||
if self.match(name):
|
if self.match(normcase(name)):
|
||||||
path = parent_path._make_child_relpath(name)
|
path = parent_path._make_child_relpath(name)
|
||||||
for p in self.successor._select_from(path, is_dir, exists, scandir):
|
for p in self.successor._select_from(path, is_dir, exists, scandir, normcase):
|
||||||
yield p
|
yield p
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
return
|
return
|
||||||
|
@ -323,13 +188,13 @@ class _RecursiveWildcardSelector(_Selector):
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
return
|
return
|
||||||
|
|
||||||
def _select_from(self, parent_path, is_dir, exists, scandir):
|
def _select_from(self, parent_path, is_dir, exists, scandir, normcase):
|
||||||
try:
|
try:
|
||||||
yielded = set()
|
yielded = set()
|
||||||
try:
|
try:
|
||||||
successor_select = self.successor._select_from
|
successor_select = self.successor._select_from
|
||||||
for starting_point in self._iterate_directories(parent_path, is_dir, scandir):
|
for starting_point in self._iterate_directories(parent_path, is_dir, scandir):
|
||||||
for p in successor_select(starting_point, is_dir, exists, scandir):
|
for p in successor_select(starting_point, is_dir, exists, scandir, normcase):
|
||||||
if p not in yielded:
|
if p not in yielded:
|
||||||
yield p
|
yield p
|
||||||
yielded.add(p)
|
yielded.add(p)
|
||||||
|
@ -387,8 +252,9 @@ class PurePath(object):
|
||||||
"""
|
"""
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
'_drv', '_root', '_parts',
|
'_drv', '_root', '_parts',
|
||||||
'_str', '_hash', '_pparts', '_cached_cparts',
|
'_str', '_hash', '_parts_tuple', '_parts_normcase_cached',
|
||||||
)
|
)
|
||||||
|
_flavour = os.path
|
||||||
|
|
||||||
def __new__(cls, *args):
|
def __new__(cls, *args):
|
||||||
"""Construct a PurePath from one or several strings and or existing
|
"""Construct a PurePath from one or several strings and or existing
|
||||||
|
@ -405,6 +271,33 @@ class PurePath(object):
|
||||||
# when pickling related paths.
|
# when pickling related paths.
|
||||||
return (self.__class__, tuple(self._parts))
|
return (self.__class__, tuple(self._parts))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _split_root(cls, part):
|
||||||
|
sep = cls._flavour.sep
|
||||||
|
rel = cls._flavour.splitdrive(part)[1].lstrip(sep)
|
||||||
|
anchor = part.removesuffix(rel)
|
||||||
|
if anchor:
|
||||||
|
anchor = cls._flavour.normpath(anchor)
|
||||||
|
drv, root = cls._flavour.splitdrive(anchor)
|
||||||
|
if drv.startswith(sep):
|
||||||
|
# UNC paths always have a root.
|
||||||
|
root = sep
|
||||||
|
return drv, root, rel
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _parse_parts(cls, parts):
|
||||||
|
if not parts:
|
||||||
|
return '', '', []
|
||||||
|
sep = cls._flavour.sep
|
||||||
|
altsep = cls._flavour.altsep
|
||||||
|
path = cls._flavour.join(*parts)
|
||||||
|
if altsep:
|
||||||
|
path = path.replace(altsep, sep)
|
||||||
|
drv, root, rel = cls._split_root(path)
|
||||||
|
unfiltered_parsed = [drv + root] + rel.split(sep)
|
||||||
|
parsed = [sys.intern(x) for x in unfiltered_parsed if x and x != '.']
|
||||||
|
return drv, root, parsed
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _parse_args(cls, args):
|
def _parse_args(cls, args):
|
||||||
# This is useful when you don't want to create an instance, just
|
# This is useful when you don't want to create an instance, just
|
||||||
|
@ -423,7 +316,7 @@ class PurePath(object):
|
||||||
"argument should be a str object or an os.PathLike "
|
"argument should be a str object or an os.PathLike "
|
||||||
"object returning str, not %r"
|
"object returning str, not %r"
|
||||||
% type(a))
|
% type(a))
|
||||||
return cls._flavour.parse_parts(parts)
|
return cls._parse_parts(parts)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_parts(cls, args):
|
def _from_parts(cls, args):
|
||||||
|
@ -447,15 +340,9 @@ class PurePath(object):
|
||||||
@classmethod
|
@classmethod
|
||||||
def _format_parsed_parts(cls, drv, root, parts):
|
def _format_parsed_parts(cls, drv, root, parts):
|
||||||
if drv or root:
|
if drv or root:
|
||||||
return drv + root + cls._flavour.join(parts[1:])
|
return drv + root + cls._flavour.sep.join(parts[1:])
|
||||||
else:
|
else:
|
||||||
return cls._flavour.join(parts)
|
return cls._flavour.sep.join(parts)
|
||||||
|
|
||||||
def _make_child(self, args):
|
|
||||||
drv, root, parts = self._parse_args(args)
|
|
||||||
drv, root, parts = self._flavour.join_parsed_parts(
|
|
||||||
self._drv, self._root, self._parts, drv, root, parts)
|
|
||||||
return self._from_parsed_parts(drv, root, parts)
|
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Return the string representation of the path, suitable for
|
"""Return the string representation of the path, suitable for
|
||||||
|
@ -488,48 +375,62 @@ class PurePath(object):
|
||||||
"""Return the path as a 'file' URI."""
|
"""Return the path as a 'file' URI."""
|
||||||
if not self.is_absolute():
|
if not self.is_absolute():
|
||||||
raise ValueError("relative path can't be expressed as a file URI")
|
raise ValueError("relative path can't be expressed as a file URI")
|
||||||
return self._flavour.make_uri(self)
|
|
||||||
|
drive = self._drv
|
||||||
|
if len(drive) == 2 and drive[1] == ':':
|
||||||
|
# It's a path on a local drive => 'file:///c:/a/b'
|
||||||
|
prefix = 'file:///' + drive
|
||||||
|
path = self.as_posix()[2:]
|
||||||
|
elif drive:
|
||||||
|
# It's a path on a network drive => 'file://host/share/a/b'
|
||||||
|
prefix = 'file:'
|
||||||
|
path = self.as_posix()
|
||||||
|
else:
|
||||||
|
# It's a posix path => 'file:///etc/hosts'
|
||||||
|
prefix = 'file://'
|
||||||
|
path = str(self)
|
||||||
|
return prefix + urlquote_from_bytes(os.fsencode(path))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _cparts(self):
|
def _parts_normcase(self):
|
||||||
# Cached casefolded parts, for hashing and comparison
|
# Cached parts with normalized case, for hashing and comparison.
|
||||||
try:
|
try:
|
||||||
return self._cached_cparts
|
return self._parts_normcase_cached
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self._cached_cparts = self._flavour.casefold_parts(self._parts)
|
self._parts_normcase_cached = [self._flavour.normcase(p) for p in self._parts]
|
||||||
return self._cached_cparts
|
return self._parts_normcase_cached
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
if not isinstance(other, PurePath):
|
if not isinstance(other, PurePath):
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
return self._cparts == other._cparts and self._flavour is other._flavour
|
return self._parts_normcase == other._parts_normcase and self._flavour is other._flavour
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
try:
|
try:
|
||||||
return self._hash
|
return self._hash
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self._hash = hash(tuple(self._cparts))
|
self._hash = hash(tuple(self._parts_normcase))
|
||||||
return self._hash
|
return self._hash
|
||||||
|
|
||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
|
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
return self._cparts < other._cparts
|
return self._parts_normcase < other._parts_normcase
|
||||||
|
|
||||||
def __le__(self, other):
|
def __le__(self, other):
|
||||||
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
|
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
return self._cparts <= other._cparts
|
return self._parts_normcase <= other._parts_normcase
|
||||||
|
|
||||||
def __gt__(self, other):
|
def __gt__(self, other):
|
||||||
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
|
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
return self._cparts > other._cparts
|
return self._parts_normcase > other._parts_normcase
|
||||||
|
|
||||||
def __ge__(self, other):
|
def __ge__(self, other):
|
||||||
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
|
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
return self._cparts >= other._cparts
|
return self._parts_normcase >= other._parts_normcase
|
||||||
|
|
||||||
drive = property(attrgetter('_drv'),
|
drive = property(attrgetter('_drv'),
|
||||||
doc="""The drive prefix (letter or UNC path), if any.""")
|
doc="""The drive prefix (letter or UNC path), if any.""")
|
||||||
|
@ -592,7 +493,7 @@ class PurePath(object):
|
||||||
"""Return a new path with the file name changed."""
|
"""Return a new path with the file name changed."""
|
||||||
if not self.name:
|
if not self.name:
|
||||||
raise ValueError("%r has an empty name" % (self,))
|
raise ValueError("%r has an empty name" % (self,))
|
||||||
drv, root, parts = self._flavour.parse_parts((name,))
|
drv, root, parts = self._parse_parts((name,))
|
||||||
if (not name or name[-1] in [self._flavour.sep, self._flavour.altsep]
|
if (not name or name[-1] in [self._flavour.sep, self._flavour.altsep]
|
||||||
or drv or root or len(parts) != 1):
|
or drv or root or len(parts) != 1):
|
||||||
raise ValueError("Invalid name %r" % (name))
|
raise ValueError("Invalid name %r" % (name))
|
||||||
|
@ -669,10 +570,10 @@ class PurePath(object):
|
||||||
# We cache the tuple to avoid building a new one each time .parts
|
# We cache the tuple to avoid building a new one each time .parts
|
||||||
# is accessed. XXX is this necessary?
|
# is accessed. XXX is this necessary?
|
||||||
try:
|
try:
|
||||||
return self._pparts
|
return self._parts_tuple
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
self._pparts = tuple(self._parts)
|
self._parts_tuple = tuple(self._parts)
|
||||||
return self._pparts
|
return self._parts_tuple
|
||||||
|
|
||||||
def joinpath(self, *args):
|
def joinpath(self, *args):
|
||||||
"""Combine this path with one or several arguments, and return a
|
"""Combine this path with one or several arguments, and return a
|
||||||
|
@ -680,11 +581,26 @@ class PurePath(object):
|
||||||
paths) or a totally different path (if one of the arguments is
|
paths) or a totally different path (if one of the arguments is
|
||||||
anchored).
|
anchored).
|
||||||
"""
|
"""
|
||||||
return self._make_child(args)
|
drv1, root1, parts1 = self._drv, self._root, self._parts
|
||||||
|
drv2, root2, parts2 = self._parse_args(args)
|
||||||
|
if root2:
|
||||||
|
if not drv2 and drv1:
|
||||||
|
return self._from_parsed_parts(drv1, root2, [drv1 + root2] + parts2[1:])
|
||||||
|
else:
|
||||||
|
return self._from_parsed_parts(drv2, root2, parts2)
|
||||||
|
elif drv2:
|
||||||
|
if drv2 == drv1 or self._flavour.normcase(drv2) == self._flavour.normcase(drv1):
|
||||||
|
# Same drive => second path is relative to the first.
|
||||||
|
return self._from_parsed_parts(drv1, root1, parts1 + parts2[1:])
|
||||||
|
else:
|
||||||
|
return self._from_parsed_parts(drv2, root2, parts2)
|
||||||
|
else:
|
||||||
|
# Second path is non-anchored (common case).
|
||||||
|
return self._from_parsed_parts(drv1, root1, parts1 + parts2)
|
||||||
|
|
||||||
def __truediv__(self, key):
|
def __truediv__(self, key):
|
||||||
try:
|
try:
|
||||||
return self._make_child((key,))
|
return self.joinpath(key)
|
||||||
except TypeError:
|
except TypeError:
|
||||||
return NotImplemented
|
return NotImplemented
|
||||||
|
|
||||||
|
@ -712,29 +628,40 @@ class PurePath(object):
|
||||||
def is_absolute(self):
|
def is_absolute(self):
|
||||||
"""True if the path is absolute (has both a root and, if applicable,
|
"""True if the path is absolute (has both a root and, if applicable,
|
||||||
a drive)."""
|
a drive)."""
|
||||||
if not self._root:
|
# ntpath.isabs() is defective - see GH-44626 .
|
||||||
return False
|
if self._flavour is ntpath:
|
||||||
return not self._flavour.has_drv or bool(self._drv)
|
return bool(self._drv and self._root)
|
||||||
|
return self._flavour.isabs(self)
|
||||||
|
|
||||||
def is_reserved(self):
|
def is_reserved(self):
|
||||||
"""Return True if the path contains one of the special names reserved
|
"""Return True if the path contains one of the special names reserved
|
||||||
by the system, if any."""
|
by the system, if any."""
|
||||||
return self._flavour.is_reserved(self._parts)
|
if self._flavour is posixpath or not self._parts:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# NOTE: the rules for reserved names seem somewhat complicated
|
||||||
|
# (e.g. r"..\NUL" is reserved but not r"foo\NUL" if "foo" does not
|
||||||
|
# exist). We err on the side of caution and return True for paths
|
||||||
|
# which are not considered reserved by Windows.
|
||||||
|
if self._parts[0].startswith('\\\\'):
|
||||||
|
# UNC paths are never reserved.
|
||||||
|
return False
|
||||||
|
name = self._parts[-1].partition('.')[0].partition(':')[0].rstrip(' ')
|
||||||
|
return name.upper() in _WIN_RESERVED_NAMES
|
||||||
|
|
||||||
def match(self, path_pattern):
|
def match(self, path_pattern):
|
||||||
"""
|
"""
|
||||||
Return True if this path matches the given pattern.
|
Return True if this path matches the given pattern.
|
||||||
"""
|
"""
|
||||||
cf = self._flavour.casefold
|
path_pattern = self._flavour.normcase(path_pattern)
|
||||||
path_pattern = cf(path_pattern)
|
drv, root, pat_parts = self._parse_parts((path_pattern,))
|
||||||
drv, root, pat_parts = self._flavour.parse_parts((path_pattern,))
|
|
||||||
if not pat_parts:
|
if not pat_parts:
|
||||||
raise ValueError("empty pattern")
|
raise ValueError("empty pattern")
|
||||||
if drv and drv != cf(self._drv):
|
elif drv and drv != self._flavour.normcase(self._drv):
|
||||||
return False
|
return False
|
||||||
if root and root != cf(self._root):
|
elif root and root != self._root:
|
||||||
return False
|
return False
|
||||||
parts = self._cparts
|
parts = self._parts_normcase
|
||||||
if drv or root:
|
if drv or root:
|
||||||
if len(pat_parts) != len(parts):
|
if len(pat_parts) != len(parts):
|
||||||
return False
|
return False
|
||||||
|
@ -757,7 +684,7 @@ class PurePosixPath(PurePath):
|
||||||
On a POSIX system, instantiating a PurePath should return this object.
|
On a POSIX system, instantiating a PurePath should return this object.
|
||||||
However, you can also instantiate it directly on any system.
|
However, you can also instantiate it directly on any system.
|
||||||
"""
|
"""
|
||||||
_flavour = _posix_flavour
|
_flavour = posixpath
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
|
|
||||||
|
@ -767,7 +694,7 @@ class PureWindowsPath(PurePath):
|
||||||
On a Windows system, instantiating a PurePath should return this object.
|
On a Windows system, instantiating a PurePath should return this object.
|
||||||
However, you can also instantiate it directly on any system.
|
However, you can also instantiate it directly on any system.
|
||||||
"""
|
"""
|
||||||
_flavour = _windows_flavour
|
_flavour = ntpath
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
|
|
||||||
|
@ -789,7 +716,7 @@ class Path(PurePath):
|
||||||
if cls is Path:
|
if cls is Path:
|
||||||
cls = WindowsPath if os.name == 'nt' else PosixPath
|
cls = WindowsPath if os.name == 'nt' else PosixPath
|
||||||
self = cls._from_parts(args)
|
self = cls._from_parts(args)
|
||||||
if not self._flavour.is_supported:
|
if self._flavour is not os.path:
|
||||||
raise NotImplementedError("cannot instantiate %r on your system"
|
raise NotImplementedError("cannot instantiate %r on your system"
|
||||||
% (cls.__name__,))
|
% (cls.__name__,))
|
||||||
return self
|
return self
|
||||||
|
@ -842,7 +769,7 @@ class Path(PurePath):
|
||||||
other_st = other_path.stat()
|
other_st = other_path.stat()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
other_st = self.__class__(other_path).stat()
|
other_st = self.__class__(other_path).stat()
|
||||||
return os.path.samestat(st, other_st)
|
return self._flavour.samestat(st, other_st)
|
||||||
|
|
||||||
def iterdir(self):
|
def iterdir(self):
|
||||||
"""Yield path objects of the directory contents.
|
"""Yield path objects of the directory contents.
|
||||||
|
@ -866,7 +793,7 @@ class Path(PurePath):
|
||||||
sys.audit("pathlib.Path.glob", self, pattern)
|
sys.audit("pathlib.Path.glob", self, pattern)
|
||||||
if not pattern:
|
if not pattern:
|
||||||
raise ValueError("Unacceptable pattern: {!r}".format(pattern))
|
raise ValueError("Unacceptable pattern: {!r}".format(pattern))
|
||||||
drv, root, pattern_parts = self._flavour.parse_parts((pattern,))
|
drv, root, pattern_parts = self._parse_parts((pattern,))
|
||||||
if drv or root:
|
if drv or root:
|
||||||
raise NotImplementedError("Non-relative patterns are unsupported")
|
raise NotImplementedError("Non-relative patterns are unsupported")
|
||||||
if pattern[-1] in (self._flavour.sep, self._flavour.altsep):
|
if pattern[-1] in (self._flavour.sep, self._flavour.altsep):
|
||||||
|
@ -881,7 +808,7 @@ class Path(PurePath):
|
||||||
this subtree.
|
this subtree.
|
||||||
"""
|
"""
|
||||||
sys.audit("pathlib.Path.rglob", self, pattern)
|
sys.audit("pathlib.Path.rglob", self, pattern)
|
||||||
drv, root, pattern_parts = self._flavour.parse_parts((pattern,))
|
drv, root, pattern_parts = self._parse_parts((pattern,))
|
||||||
if drv or root:
|
if drv or root:
|
||||||
raise NotImplementedError("Non-relative patterns are unsupported")
|
raise NotImplementedError("Non-relative patterns are unsupported")
|
||||||
if pattern and pattern[-1] in (self._flavour.sep, self._flavour.altsep):
|
if pattern and pattern[-1] in (self._flavour.sep, self._flavour.altsep):
|
||||||
|
@ -912,7 +839,7 @@ class Path(PurePath):
|
||||||
raise RuntimeError("Symlink loop from %r" % e.filename)
|
raise RuntimeError("Symlink loop from %r" % e.filename)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
s = os.path.realpath(self, strict=strict)
|
s = self._flavour.realpath(self, strict=strict)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
check_eloop(e)
|
check_eloop(e)
|
||||||
raise
|
raise
|
||||||
|
@ -1184,7 +1111,7 @@ class Path(PurePath):
|
||||||
"""
|
"""
|
||||||
Check if this path is a mount point
|
Check if this path is a mount point
|
||||||
"""
|
"""
|
||||||
return self._flavour.pathmod.ismount(self)
|
return self._flavour.ismount(self)
|
||||||
|
|
||||||
def is_symlink(self):
|
def is_symlink(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1205,7 +1132,7 @@ class Path(PurePath):
|
||||||
"""
|
"""
|
||||||
Whether this path is a junction.
|
Whether this path is a junction.
|
||||||
"""
|
"""
|
||||||
return self._flavour.pathmod.isjunction(self)
|
return self._flavour.isjunction(self)
|
||||||
|
|
||||||
def is_block_device(self):
|
def is_block_device(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1277,7 +1204,7 @@ class Path(PurePath):
|
||||||
"""
|
"""
|
||||||
if (not (self._drv or self._root) and
|
if (not (self._drv or self._root) and
|
||||||
self._parts and self._parts[0][:1] == '~'):
|
self._parts and self._parts[0][:1] == '~'):
|
||||||
homedir = os.path.expanduser(self._parts[0])
|
homedir = self._flavour.expanduser(self._parts[0])
|
||||||
if homedir[:1] == "~":
|
if homedir[:1] == "~":
|
||||||
raise RuntimeError("Could not determine home directory.")
|
raise RuntimeError("Could not determine home directory.")
|
||||||
return self._from_parts([homedir] + self._parts[1:])
|
return self._from_parts([homedir] + self._parts[1:])
|
||||||
|
|
|
@ -26,7 +26,7 @@ except ImportError:
|
||||||
class _BaseFlavourTest(object):
|
class _BaseFlavourTest(object):
|
||||||
|
|
||||||
def _check_parse_parts(self, arg, expected):
|
def _check_parse_parts(self, arg, expected):
|
||||||
f = self.flavour.parse_parts
|
f = self.cls._parse_parts
|
||||||
sep = self.flavour.sep
|
sep = self.flavour.sep
|
||||||
altsep = self.flavour.altsep
|
altsep = self.flavour.altsep
|
||||||
actual = f([x.replace('/', sep) for x in arg])
|
actual = f([x.replace('/', sep) for x in arg])
|
||||||
|
@ -65,7 +65,8 @@ class _BaseFlavourTest(object):
|
||||||
|
|
||||||
|
|
||||||
class PosixFlavourTest(_BaseFlavourTest, unittest.TestCase):
|
class PosixFlavourTest(_BaseFlavourTest, unittest.TestCase):
|
||||||
flavour = pathlib._posix_flavour
|
cls = pathlib.PurePosixPath
|
||||||
|
flavour = pathlib.PurePosixPath._flavour
|
||||||
|
|
||||||
def test_parse_parts(self):
|
def test_parse_parts(self):
|
||||||
check = self._check_parse_parts
|
check = self._check_parse_parts
|
||||||
|
@ -80,7 +81,7 @@ class PosixFlavourTest(_BaseFlavourTest, unittest.TestCase):
|
||||||
check(['\\a'], ('', '', ['\\a']))
|
check(['\\a'], ('', '', ['\\a']))
|
||||||
|
|
||||||
def test_splitroot(self):
|
def test_splitroot(self):
|
||||||
f = self.flavour.splitroot
|
f = self.cls._split_root
|
||||||
self.assertEqual(f(''), ('', '', ''))
|
self.assertEqual(f(''), ('', '', ''))
|
||||||
self.assertEqual(f('a'), ('', '', 'a'))
|
self.assertEqual(f('a'), ('', '', 'a'))
|
||||||
self.assertEqual(f('a/b'), ('', '', 'a/b'))
|
self.assertEqual(f('a/b'), ('', '', 'a/b'))
|
||||||
|
@ -101,7 +102,8 @@ class PosixFlavourTest(_BaseFlavourTest, unittest.TestCase):
|
||||||
|
|
||||||
|
|
||||||
class NTFlavourTest(_BaseFlavourTest, unittest.TestCase):
|
class NTFlavourTest(_BaseFlavourTest, unittest.TestCase):
|
||||||
flavour = pathlib._windows_flavour
|
cls = pathlib.PureWindowsPath
|
||||||
|
flavour = pathlib.PureWindowsPath._flavour
|
||||||
|
|
||||||
def test_parse_parts(self):
|
def test_parse_parts(self):
|
||||||
check = self._check_parse_parts
|
check = self._check_parse_parts
|
||||||
|
@ -142,7 +144,7 @@ class NTFlavourTest(_BaseFlavourTest, unittest.TestCase):
|
||||||
check(['c:/a/b', 'c:/x/y'], ('c:', '\\', ['c:\\', 'x', 'y']))
|
check(['c:/a/b', 'c:/x/y'], ('c:', '\\', ['c:\\', 'x', 'y']))
|
||||||
|
|
||||||
def test_splitroot(self):
|
def test_splitroot(self):
|
||||||
f = self.flavour.splitroot
|
f = self.cls._split_root
|
||||||
self.assertEqual(f(''), ('', '', ''))
|
self.assertEqual(f(''), ('', '', ''))
|
||||||
self.assertEqual(f('a'), ('', '', 'a'))
|
self.assertEqual(f('a'), ('', '', 'a'))
|
||||||
self.assertEqual(f('a\\b'), ('', '', 'a\\b'))
|
self.assertEqual(f('a\\b'), ('', '', 'a\\b'))
|
||||||
|
@ -151,19 +153,12 @@ class NTFlavourTest(_BaseFlavourTest, unittest.TestCase):
|
||||||
self.assertEqual(f('c:a\\b'), ('c:', '', 'a\\b'))
|
self.assertEqual(f('c:a\\b'), ('c:', '', 'a\\b'))
|
||||||
self.assertEqual(f('c:\\a\\b'), ('c:', '\\', 'a\\b'))
|
self.assertEqual(f('c:\\a\\b'), ('c:', '\\', 'a\\b'))
|
||||||
# Redundant slashes in the root are collapsed.
|
# Redundant slashes in the root are collapsed.
|
||||||
self.assertEqual(f('\\\\a'), ('', '\\', 'a'))
|
|
||||||
self.assertEqual(f('\\\\\\a/b'), ('', '\\', 'a/b'))
|
|
||||||
self.assertEqual(f('c:\\\\a'), ('c:', '\\', 'a'))
|
self.assertEqual(f('c:\\\\a'), ('c:', '\\', 'a'))
|
||||||
self.assertEqual(f('c:\\\\\\a/b'), ('c:', '\\', 'a/b'))
|
self.assertEqual(f('c:\\\\\\a/b'), ('c:', '\\', 'a/b'))
|
||||||
# Valid UNC paths.
|
# Valid UNC paths.
|
||||||
self.assertEqual(f('\\\\a\\b'), ('\\\\a\\b', '\\', ''))
|
self.assertEqual(f('\\\\a\\b'), ('\\\\a\\b', '\\', ''))
|
||||||
self.assertEqual(f('\\\\a\\b\\'), ('\\\\a\\b', '\\', ''))
|
self.assertEqual(f('\\\\a\\b\\'), ('\\\\a\\b', '\\', ''))
|
||||||
self.assertEqual(f('\\\\a\\b\\c\\d'), ('\\\\a\\b', '\\', 'c\\d'))
|
self.assertEqual(f('\\\\a\\b\\c\\d'), ('\\\\a\\b', '\\', 'c\\d'))
|
||||||
# These are non-UNC paths (according to ntpath.py and test_ntpath).
|
|
||||||
# However, command.com says such paths are invalid, so it's
|
|
||||||
# difficult to know what the right semantics are.
|
|
||||||
self.assertEqual(f('\\\\\\a\\b'), ('', '\\', 'a\\b'))
|
|
||||||
self.assertEqual(f('\\\\a'), ('', '\\', 'a'))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -182,8 +177,7 @@ class _BasePurePathTest(object):
|
||||||
('', 'a', 'b'), ('a', '', 'b'), ('a', 'b', ''),
|
('', 'a', 'b'), ('a', '', 'b'), ('a', 'b', ''),
|
||||||
],
|
],
|
||||||
'/b/c/d': [
|
'/b/c/d': [
|
||||||
('a', '/b/c', 'd'), ('a', '///b//c', 'd/'),
|
('a', '/b/c', 'd'), ('/a', '/b/c', 'd'),
|
||||||
('/a', '/b/c', 'd'),
|
|
||||||
# Empty components get removed.
|
# Empty components get removed.
|
||||||
('/', 'b', '', 'c/d'), ('/', '', 'b/c/d'), ('', '/b/c/d'),
|
('/', 'b', '', 'c/d'), ('/', '', 'b/c/d'), ('', '/b/c/d'),
|
||||||
],
|
],
|
||||||
|
@ -291,19 +285,26 @@ class _BasePurePathTest(object):
|
||||||
|
|
||||||
def test_repr_common(self):
|
def test_repr_common(self):
|
||||||
for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'):
|
for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'):
|
||||||
p = self.cls(pathstr)
|
with self.subTest(pathstr=pathstr):
|
||||||
clsname = p.__class__.__name__
|
p = self.cls(pathstr)
|
||||||
r = repr(p)
|
clsname = p.__class__.__name__
|
||||||
# The repr() is in the form ClassName("forward-slashes path").
|
r = repr(p)
|
||||||
self.assertTrue(r.startswith(clsname + '('), r)
|
# The repr() is in the form ClassName("forward-slashes path").
|
||||||
self.assertTrue(r.endswith(')'), r)
|
self.assertTrue(r.startswith(clsname + '('), r)
|
||||||
inner = r[len(clsname) + 1 : -1]
|
self.assertTrue(r.endswith(')'), r)
|
||||||
self.assertEqual(eval(inner), p.as_posix())
|
inner = r[len(clsname) + 1 : -1]
|
||||||
# The repr() roundtrips.
|
self.assertEqual(eval(inner), p.as_posix())
|
||||||
q = eval(r, pathlib.__dict__)
|
|
||||||
self.assertIs(q.__class__, p.__class__)
|
def test_repr_roundtrips(self):
|
||||||
self.assertEqual(q, p)
|
for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'):
|
||||||
self.assertEqual(repr(q), r)
|
with self.subTest(pathstr=pathstr):
|
||||||
|
p = self.cls(pathstr)
|
||||||
|
r = repr(p)
|
||||||
|
# The repr() roundtrips.
|
||||||
|
q = eval(r, pathlib.__dict__)
|
||||||
|
self.assertIs(q.__class__, p.__class__)
|
||||||
|
self.assertEqual(q, p)
|
||||||
|
self.assertEqual(repr(q), r)
|
||||||
|
|
||||||
def test_eq_common(self):
|
def test_eq_common(self):
|
||||||
P = self.cls
|
P = self.cls
|
||||||
|
@ -2412,9 +2413,9 @@ class _BasePathTest(object):
|
||||||
def test_is_junction(self):
|
def test_is_junction(self):
|
||||||
P = self.cls(BASE)
|
P = self.cls(BASE)
|
||||||
|
|
||||||
with mock.patch.object(P._flavour, 'pathmod'):
|
with mock.patch.object(P._flavour, 'isjunction'):
|
||||||
self.assertEqual(P.is_junction(), P._flavour.pathmod.isjunction.return_value)
|
self.assertEqual(P.is_junction(), P._flavour.isjunction.return_value)
|
||||||
P._flavour.pathmod.isjunction.assert_called_once_with(P)
|
P._flavour.isjunction.assert_called_once_with(P)
|
||||||
|
|
||||||
def test_is_fifo_false(self):
|
def test_is_fifo_false(self):
|
||||||
P = self.cls(BASE)
|
P = self.cls(BASE)
|
||||||
|
@ -3072,6 +3073,22 @@ class WindowsPathTest(_BasePathTest, unittest.TestCase):
|
||||||
check()
|
check()
|
||||||
|
|
||||||
|
|
||||||
|
class PurePathSubclassTest(_BasePurePathTest, unittest.TestCase):
|
||||||
|
class cls(pathlib.PurePath):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# repr() roundtripping is not supported in custom subclass.
|
||||||
|
test_repr_roundtrips = None
|
||||||
|
|
||||||
|
|
||||||
|
class PathSubclassTest(_BasePathTest, unittest.TestCase):
|
||||||
|
class cls(pathlib.Path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# repr() roundtripping is not supported in custom subclass.
|
||||||
|
test_repr_roundtrips = None
|
||||||
|
|
||||||
|
|
||||||
class CompatiblePathTest(unittest.TestCase):
|
class CompatiblePathTest(unittest.TestCase):
|
||||||
"""
|
"""
|
||||||
Test that a type can be made compatible with PurePath
|
Test that a type can be made compatible with PurePath
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
Make :class:`pathlib.PurePath` and :class:`~pathlib.Path` subclassable
|
||||||
|
(private to start). Previously, attempting to instantiate a subclass
|
||||||
|
resulted in an :exc:`AttributeError` being raised. Patch by Barney Gale.
|
Loading…
Add table
Add a link
Reference in a new issue