summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Lib/pathlib.py331
-rw-r--r--Lib/test/test_pathlib.py77
-rw-r--r--Misc/NEWS.d/next/Library/2022-03-05-02-14-09.bpo-24132.W6iORO.rst3
3 files changed, 179 insertions, 232 deletions
diff --git a/Lib/pathlib.py b/Lib/pathlib.py
index 7890fda..b959e85 100644
--- a/Lib/pathlib.py
+++ b/Lib/pathlib.py
@@ -30,6 +30,14 @@ __all__ = [
# 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_INVALID_NAME = 123 # fix for bpo-35306
_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.
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
#
@@ -237,14 +101,15 @@ class _Selector:
is_dir = path_cls.is_dir
exists = path_cls.exists
scandir = path_cls._scandir
+ normcase = path_cls._flavour.normcase
if not is_dir(parent_path):
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:
- def _select_from(self, parent_path, is_dir, exists, scandir):
+ def _select_from(self, parent_path, is_dir, exists, scandir, normcase):
yield parent_path
@@ -254,11 +119,11 @@ class _PreciseSelector(_Selector):
self.name = name
_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:
path = parent_path._make_child_relpath(self.name)
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
except PermissionError:
return
@@ -267,10 +132,10 @@ class _PreciseSelector(_Selector):
class _WildcardSelector(_Selector):
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)
- def _select_from(self, parent_path, is_dir, exists, scandir):
+ def _select_from(self, parent_path, is_dir, exists, scandir, normcase):
try:
# We must close the scandir() object before proceeding to
# avoid exhausting file descriptors when globbing deep trees.
@@ -289,9 +154,9 @@ class _WildcardSelector(_Selector):
raise
continue
name = entry.name
- if self.match(name):
+ if self.match(normcase(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
except PermissionError:
return
@@ -323,13 +188,13 @@ class _RecursiveWildcardSelector(_Selector):
except PermissionError:
return
- def _select_from(self, parent_path, is_dir, exists, scandir):
+ def _select_from(self, parent_path, is_dir, exists, scandir, normcase):
try:
yielded = set()
try:
successor_select = self.successor._select_from
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:
yield p
yielded.add(p)
@@ -387,8 +252,9 @@ class PurePath(object):
"""
__slots__ = (
'_drv', '_root', '_parts',
- '_str', '_hash', '_pparts', '_cached_cparts',
+ '_str', '_hash', '_parts_tuple', '_parts_normcase_cached',
)
+ _flavour = os.path
def __new__(cls, *args):
"""Construct a PurePath from one or several strings and or existing
@@ -406,6 +272,33 @@ class PurePath(object):
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
def _parse_args(cls, args):
# This is useful when you don't want to create an instance, just
# canonicalize some constructor arguments.
@@ -423,7 +316,7 @@ class PurePath(object):
"argument should be a str object or an os.PathLike "
"object returning str, not %r"
% type(a))
- return cls._flavour.parse_parts(parts)
+ return cls._parse_parts(parts)
@classmethod
def _from_parts(cls, args):
@@ -447,15 +340,9 @@ class PurePath(object):
@classmethod
def _format_parsed_parts(cls, drv, root, parts):
if drv or root:
- return drv + root + cls._flavour.join(parts[1:])
+ return drv + root + cls._flavour.sep.join(parts[1:])
else:
- return cls._flavour.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)
+ return cls._flavour.sep.join(parts)
def __str__(self):
"""Return the string representation of the path, suitable for
@@ -488,48 +375,62 @@ class PurePath(object):
"""Return the path as a 'file' URI."""
if not self.is_absolute():
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
- def _cparts(self):
- # Cached casefolded parts, for hashing and comparison
+ def _parts_normcase(self):
+ # Cached parts with normalized case, for hashing and comparison.
try:
- return self._cached_cparts
+ return self._parts_normcase_cached
except AttributeError:
- self._cached_cparts = self._flavour.casefold_parts(self._parts)
- return self._cached_cparts
+ self._parts_normcase_cached = [self._flavour.normcase(p) for p in self._parts]
+ return self._parts_normcase_cached
def __eq__(self, other):
if not isinstance(other, PurePath):
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):
try:
return self._hash
except AttributeError:
- self._hash = hash(tuple(self._cparts))
+ self._hash = hash(tuple(self._parts_normcase))
return self._hash
def __lt__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
return NotImplemented
- return self._cparts < other._cparts
+ return self._parts_normcase < other._parts_normcase
def __le__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
return NotImplemented
- return self._cparts <= other._cparts
+ return self._parts_normcase <= other._parts_normcase
def __gt__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
return NotImplemented
- return self._cparts > other._cparts
+ return self._parts_normcase > other._parts_normcase
def __ge__(self, other):
if not isinstance(other, PurePath) or self._flavour is not other._flavour:
return NotImplemented
- return self._cparts >= other._cparts
+ return self._parts_normcase >= other._parts_normcase
drive = property(attrgetter('_drv'),
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."""
if not self.name:
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]
or drv or root or len(parts) != 1):
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
# is accessed. XXX is this necessary?
try:
- return self._pparts
+ return self._parts_tuple
except AttributeError:
- self._pparts = tuple(self._parts)
- return self._pparts
+ self._parts_tuple = tuple(self._parts)
+ return self._parts_tuple
def joinpath(self, *args):
"""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
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):
try:
- return self._make_child((key,))
+ return self.joinpath(key)
except TypeError:
return NotImplemented
@@ -712,29 +628,40 @@ class PurePath(object):
def is_absolute(self):
"""True if the path is absolute (has both a root and, if applicable,
a drive)."""
- if not self._root:
- return False
- return not self._flavour.has_drv or bool(self._drv)
+ # ntpath.isabs() is defective - see GH-44626 .
+ if self._flavour is ntpath:
+ return bool(self._drv and self._root)
+ return self._flavour.isabs(self)
def is_reserved(self):
"""Return True if the path contains one of the special names reserved
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):
"""
Return True if this path matches the given pattern.
"""
- cf = self._flavour.casefold
- path_pattern = cf(path_pattern)
- drv, root, pat_parts = self._flavour.parse_parts((path_pattern,))
+ path_pattern = self._flavour.normcase(path_pattern)
+ drv, root, pat_parts = self._parse_parts((path_pattern,))
if not pat_parts:
raise ValueError("empty pattern")
- if drv and drv != cf(self._drv):
+ elif drv and drv != self._flavour.normcase(self._drv):
return False
- if root and root != cf(self._root):
+ elif root and root != self._root:
return False
- parts = self._cparts
+ parts = self._parts_normcase
if drv or root:
if len(pat_parts) != len(parts):
return False
@@ -757,7 +684,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 = _posix_flavour
+ _flavour = posixpath
__slots__ = ()
@@ -767,7 +694,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 = _windows_flavour
+ _flavour = ntpath
__slots__ = ()
@@ -789,7 +716,7 @@ class Path(PurePath):
if cls is Path:
cls = WindowsPath if os.name == 'nt' else PosixPath
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"
% (cls.__name__,))
return self
@@ -842,7 +769,7 @@ class Path(PurePath):
other_st = other_path.stat()
except AttributeError:
other_st = self.__class__(other_path).stat()
- return os.path.samestat(st, other_st)
+ return self._flavour.samestat(st, other_st)
def iterdir(self):
"""Yield path objects of the directory contents.
@@ -866,7 +793,7 @@ class Path(PurePath):
sys.audit("pathlib.Path.glob", self, pattern)
if not 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:
raise NotImplementedError("Non-relative patterns are unsupported")
if pattern[-1] in (self._flavour.sep, self._flavour.altsep):
@@ -881,7 +808,7 @@ class Path(PurePath):
this subtree.
"""
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:
raise NotImplementedError("Non-relative patterns are unsupported")
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)
try:
- s = os.path.realpath(self, strict=strict)
+ s = self._flavour.realpath(self, strict=strict)
except OSError as e:
check_eloop(e)
raise
@@ -1184,7 +1111,7 @@ class Path(PurePath):
"""
Check if this path is a mount point
"""
- return self._flavour.pathmod.ismount(self)
+ return self._flavour.ismount(self)
def is_symlink(self):
"""
@@ -1205,7 +1132,7 @@ class Path(PurePath):
"""
Whether this path is a junction.
"""
- return self._flavour.pathmod.isjunction(self)
+ return self._flavour.isjunction(self)
def is_block_device(self):
"""
@@ -1277,7 +1204,7 @@ class Path(PurePath):
"""
if (not (self._drv or self._root) and
self._parts and self._parts[0][:1] == '~'):
- homedir = os.path.expanduser(self._parts[0])
+ homedir = self._flavour.expanduser(self._parts[0])
if homedir[:1] == "~":
raise RuntimeError("Could not determine home directory.")
return self._from_parts([homedir] + self._parts[1:])
diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py
index fa6ea0a..7d4d782 100644
--- a/Lib/test/test_pathlib.py
+++ b/Lib/test/test_pathlib.py
@@ -26,7 +26,7 @@ except ImportError:
class _BaseFlavourTest(object):
def _check_parse_parts(self, arg, expected):
- f = self.flavour.parse_parts
+ f = self.cls._parse_parts
sep = self.flavour.sep
altsep = self.flavour.altsep
actual = f([x.replace('/', sep) for x in arg])
@@ -65,7 +65,8 @@ class _BaseFlavourTest(object):
class PosixFlavourTest(_BaseFlavourTest, unittest.TestCase):
- flavour = pathlib._posix_flavour
+ cls = pathlib.PurePosixPath
+ flavour = pathlib.PurePosixPath._flavour
def test_parse_parts(self):
check = self._check_parse_parts
@@ -80,7 +81,7 @@ class PosixFlavourTest(_BaseFlavourTest, unittest.TestCase):
check(['\\a'], ('', '', ['\\a']))
def test_splitroot(self):
- f = self.flavour.splitroot
+ f = self.cls._split_root
self.assertEqual(f(''), ('', '', ''))
self.assertEqual(f('a'), ('', '', 'a'))
self.assertEqual(f('a/b'), ('', '', 'a/b'))
@@ -101,7 +102,8 @@ class PosixFlavourTest(_BaseFlavourTest, unittest.TestCase):
class NTFlavourTest(_BaseFlavourTest, unittest.TestCase):
- flavour = pathlib._windows_flavour
+ cls = pathlib.PureWindowsPath
+ flavour = pathlib.PureWindowsPath._flavour
def test_parse_parts(self):
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']))
def test_splitroot(self):
- f = self.flavour.splitroot
+ f = self.cls._split_root
self.assertEqual(f(''), ('', '', ''))
self.assertEqual(f('a'), ('', '', 'a'))
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'))
# 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/b'), ('c:', '\\', 'a/b'))
# Valid UNC paths.
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'))
- # 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', ''),
],
'/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.
('/', 'b', '', 'c/d'), ('/', '', 'b/c/d'), ('', '/b/c/d'),
],
@@ -291,19 +285,26 @@ class _BasePurePathTest(object):
def test_repr_common(self):
for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'):
- p = self.cls(pathstr)
- clsname = p.__class__.__name__
- r = repr(p)
- # The repr() is in the form ClassName("forward-slashes path").
- self.assertTrue(r.startswith(clsname + '('), r)
- self.assertTrue(r.endswith(')'), r)
- inner = r[len(clsname) + 1 : -1]
- self.assertEqual(eval(inner), p.as_posix())
- # The repr() roundtrips.
- q = eval(r, pathlib.__dict__)
- self.assertIs(q.__class__, p.__class__)
- self.assertEqual(q, p)
- self.assertEqual(repr(q), r)
+ with self.subTest(pathstr=pathstr):
+ p = self.cls(pathstr)
+ clsname = p.__class__.__name__
+ r = repr(p)
+ # The repr() is in the form ClassName("forward-slashes path").
+ self.assertTrue(r.startswith(clsname + '('), r)
+ self.assertTrue(r.endswith(')'), r)
+ inner = r[len(clsname) + 1 : -1]
+ self.assertEqual(eval(inner), p.as_posix())
+
+ def test_repr_roundtrips(self):
+ for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'):
+ 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):
P = self.cls
@@ -2412,9 +2413,9 @@ class _BasePathTest(object):
def test_is_junction(self):
P = self.cls(BASE)
- with mock.patch.object(P._flavour, 'pathmod'):
- self.assertEqual(P.is_junction(), P._flavour.pathmod.isjunction.return_value)
- P._flavour.pathmod.isjunction.assert_called_once_with(P)
+ 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)
def test_is_fifo_false(self):
P = self.cls(BASE)
@@ -3072,6 +3073,22 @@ class WindowsPathTest(_BasePathTest, unittest.TestCase):
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):
"""
Test that a type can be made compatible with PurePath
diff --git a/Misc/NEWS.d/next/Library/2022-03-05-02-14-09.bpo-24132.W6iORO.rst b/Misc/NEWS.d/next/Library/2022-03-05-02-14-09.bpo-24132.W6iORO.rst
new file mode 100644
index 0000000..8ca5213
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2022-03-05-02-14-09.bpo-24132.W6iORO.rst
@@ -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.