diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index 60791725c2323d..9aac2242159ea7 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -145,6 +145,15 @@ we also call *flavours*: >>> PureWindowsPath('c:/Windows', '/Program Files') PureWindowsPath('c:/Program Files') + At most one trailing slash is kept:: + + >>> PurePath('foo//') + PurePosixPath('foo/') + + .. versionchanged:: 3.13 + A trailing slash is now retained, as it is meaningful to path + resolution. + Spurious slashes and single dots are collapsed, but double dots (``'..'``) and leading double slashes (``'//'``) are not, since this would change the meaning of a path for various reasons (e.g. symbolic links, UNC paths):: @@ -153,8 +162,8 @@ we also call *flavours*: PurePosixPath('foo/bar') >>> PurePath('//foo/bar') PurePosixPath('//foo/bar') - >>> PurePath('foo/./bar') - PurePosixPath('foo/bar') + >>> PurePath('foo/./bar/.') + PurePosixPath('foo/bar/') >>> PurePath('foo/../bar') PurePosixPath('foo/../bar') @@ -184,7 +193,7 @@ we also call *flavours*: filesystem paths, including `UNC paths`_:: >>> PureWindowsPath('c:/Program Files/') - PureWindowsPath('c:/Program Files') + PureWindowsPath('c:/Program Files/') >>> PureWindowsPath('//server/share/file') PureWindowsPath('//server/share/file') @@ -376,6 +385,21 @@ Pure paths provide the following methods and properties: '\\\\host\\share\\' +.. attribute:: PurePath.has_trailing_sep + + Whether the path has a trailing slash after its :attr:`name`:: + + >>> PurePosixPath('foo/bar/').has_trailing_sep + True + + If the path has no name, this property is false:: + + >>> PureWindowsPath('c:/').has_trailing_sep + False + + .. versionadded:: 3.13 + + .. attribute:: PurePath.parents An immutable sequence providing access to the logical ancestors of @@ -614,6 +638,10 @@ Pure paths provide the following methods and properties: Support for the recursive wildcard "``**``" was added. In previous versions, it acted like the non-recursive wildcard "``*``". + .. versionchanged:: 3.13 + Matching now considers whether the path and *pattern* end with path + separators. + .. method:: PurePath.relative_to(other, walk_up=False) @@ -719,6 +747,41 @@ Pure paths provide the following methods and properties: PureWindowsPath('README') +.. method:: PurePath.with_trailing_sep() + + Return a new path with a trailing slash after its :attr:`name`. If the + original path doesn't have a name, :exc:`ValueError` is raised:: + + >>> p = PureWindowsPath('c:/windows') + >>> p.with_trailing_sep() + PureWindowsPath('c:/windows/') + >>> p = PureWindowsPath('c:/') + >>> p.with_trailing_sep() + Traceback (most recent call last): + File "", line 1, in + p.with_trailing_sep() + ~~~~~~~~~~~~~~~~~~~^^ + File "/home/barney/projects/cpython/Lib/pathlib.py", line 459, in with_trailing_sep + raise ValueError(f"{self!r} has an empty name") + ValueError: PureWindowsPath('c:/') has an empty name + + .. versionadded:: 3.13 + + +.. method:: PurePath.without_trailing_sep() + + Return a new path without a slash after its :attr:`name`, if any:: + + >>> p = PureWindowsPath('c:/windows/') + >>> p.without_trailing_sep() + PureWindowsPath('c:/windows') + >>> p = PureWindowsPath('c:/') + >>> p.without_trailing_sep() + PureWindowsPath('c:/') + + .. versionadded:: 3.13 + + .. method:: PurePath.with_segments(*pathsegments) Create a new path object of the same type by combining the given @@ -1020,6 +1083,11 @@ call fails (for example because the path doesn't exist). future Python release, patterns with this ending will match both files and directories. Add a trailing slash to match only directories. + .. versionchanged:: 3.13 + Returns paths with trailing path separators if *pattern* also ends with + a pathname components separator (:data:`~os.sep` or :data:`~os.altsep`). + + .. method:: Path.group(*, follow_symlinks=True) Return the name of the group owning the file. :exc:`KeyError` is raised diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index b020d2db350da8..0d8a84040d6bc7 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -89,7 +89,8 @@ def __init__(self, *args): "argument should be a str or an os.PathLike " "object where __fspath__ returns a str, " f"not {type(path).__name__!r}") - paths.append(path) + if path: + paths.append(path) # Avoid calling super().__init__, as an optimisation self._raw_paths = paths self._resolving = False @@ -287,7 +288,7 @@ def absolute(self): return self if self.root: drive = os.path.splitroot(os.getcwd())[0] - return self._from_parsed_parts(drive, self.root, self._tail) + return self._from_parsed_parts(drive, self.root, self._tail, self.has_trailing_sep) if self.drive: # There is a CWD on each drive-letter drive. cwd = os.path.abspath(self.drive) @@ -304,10 +305,10 @@ def absolute(self): return result drive, root, rel = os.path.splitroot(cwd) if not rel: - return self._from_parsed_parts(drive, root, self._tail) + return self._from_parsed_parts(drive, root, self._tail, self.has_trailing_sep) tail = rel.split(self.pathmod.sep) tail.extend(self._tail) - return self._from_parsed_parts(drive, root, tail) + return self._from_parsed_parts(drive, root, tail, self.has_trailing_sep) def resolve(self, strict=False): """ @@ -454,8 +455,10 @@ def expanduser(self): homedir = os.path.expanduser(self._tail[0]) if homedir[:1] == "~": raise RuntimeError("Could not determine home directory.") - drv, root, tail = self._parse_path(homedir) - return self._from_parsed_parts(drv, root, tail + self._tail[1:]) + drv, root, tail, _ = self._parse_path(homedir) + tail.extend(self._tail[1:]) + has_trailing_sep = self.has_trailing_sep and bool(tail) + return self._from_parsed_parts(drv, root, tail, has_trailing_sep) return self diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py index 4808d0e61f7038..e09535503f7610 100644 --- a/Lib/pathlib/_abc.py +++ b/Lib/pathlib/_abc.py @@ -69,6 +69,12 @@ def _compile_pattern(pat, sep, case_sensitive): return re.compile(regex, flags=flags).match +def _select_parents(paths, dir_only): + """Yield lexical parents of the given paths.""" + for path in paths: + yield path._make_child_relpath('..', dir_only) + + def _select_children(parent_paths, dir_only, follow_symlinks, match): """Yield direct children of given paths, filtering by name and type.""" if follow_symlinks is None: @@ -91,7 +97,7 @@ def _select_children(parent_paths, dir_only, follow_symlinks, match): continue name = entry.name if match(name): - yield parent_path._make_child_relpath(name) + yield parent_path._make_child_relpath(name, dir_only) def _select_recursive(parent_paths, dir_only, follow_symlinks): @@ -114,7 +120,7 @@ def _select_recursive(parent_paths, dir_only, follow_symlinks): for entry in entries: try: if entry.is_dir(follow_symlinks=follow_symlinks): - paths.append(path._make_child_relpath(entry.name)) + paths.append(path._make_child_relpath(entry.name, dir_only)) continue except OSError: pass @@ -194,6 +200,10 @@ class PurePathBase: # tail are normalized. '_drv', '_root', '_tail_cached', + # The `_has_trailing_sep` slot stores a boolean indicating whether a + # trailing slash follows the path name. + '_has_trailing_sep', + # The `_str` slot stores the string representation of the path, # computed from the drive, root and tail when `__str__()` is called # for the first time. It's used to implement `_str_normcase` @@ -206,7 +216,11 @@ class PurePathBase: ) pathmod = os.path - def __init__(self, *paths): + def __init__(self, *args): + paths = [] + for arg in args: + if arg: + paths.append(arg) self._raw_paths = paths self._resolving = False @@ -220,7 +234,7 @@ def with_segments(self, *pathsegments): @classmethod def _parse_path(cls, path): if not path: - return '', '', [] + return '', '', [], False sep = cls.pathmod.sep altsep = cls.pathmod.altsep if altsep: @@ -235,7 +249,8 @@ def _parse_path(cls, path): # e.g. //?/unc/server/share root = sep parsed = [sys.intern(str(x)) for x in rel.split(sep) if x and x != '.'] - return drv, root, parsed + has_trailing_sep = bool(parsed) and not rel.endswith(parsed[-1]) + return drv, root, parsed, has_trailing_sep def _load_parts(self): paths = self._raw_paths @@ -245,22 +260,26 @@ def _load_parts(self): path = paths[0] else: path = self.pathmod.join(*paths) - drv, root, tail = self._parse_path(path) + drv, root, tail, has_trailing_sep = self._parse_path(path) self._drv = drv self._root = root self._tail_cached = tail + self._has_trailing_sep = has_trailing_sep - def _from_parsed_parts(self, drv, root, tail): - path_str = self._format_parsed_parts(drv, root, tail) + def _from_parsed_parts(self, drv, root, tail, has_trailing_sep=False): + path_str = self._format_parsed_parts(drv, root, tail, has_trailing_sep) path = self.with_segments(path_str) path._str = path_str or '.' path._drv = drv path._root = root path._tail_cached = tail + path._has_trailing_sep = has_trailing_sep return path @classmethod - def _format_parsed_parts(cls, drv, root, tail): + def _format_parsed_parts(cls, drv, root, tail, has_trailing_sep): + if has_trailing_sep: + tail = tail + [''] if drv or root: return drv + root + cls.pathmod.sep.join(tail) elif tail and cls.pathmod.splitdrive(tail[0])[0]: @@ -274,7 +293,7 @@ def __str__(self): return self._str except AttributeError: self._str = self._format_parsed_parts(self.drive, self.root, - self._tail) or '.' + self._tail, self.has_trailing_sep) or '.' return self._str def as_posix(self): @@ -311,6 +330,15 @@ def _tail(self): self._load_parts() return self._tail_cached + @property + def has_trailing_sep(self): + """True if the path has a trailing slash after its name.""" + try: + return self._has_trailing_sep + except AttributeError: + self._load_parts() + return self._has_trailing_sep + @property def anchor(self): """The concatenation of the drive and root, or ''.""" @@ -371,7 +399,7 @@ def with_name(self, name): if not tail: raise ValueError(f"{self!r} has an empty name") tail[-1] = name - return self._from_parsed_parts(self.drive, self.root, tail) + return self._from_parsed_parts(self.drive, self.root, tail, self.has_trailing_sep) def with_stem(self, stem): """Return a new path with the stem changed.""" @@ -389,6 +417,28 @@ def with_suffix(self, suffix): else: raise ValueError(f"Invalid suffix {suffix!r}") + def with_trailing_sep(self): + """Return a new path with a trailing slash after its name. If the + path has no name, ValueError is raised.""" + if self.has_trailing_sep: + return self + tail = self._tail + if not tail: + raise ValueError(f"{self!r} has an empty name") + return self._from_parsed_parts(self.drive, self.root, tail, True) + + def without_trailing_sep(self): + """Return a new path without a trailing slash after its name.""" + if not self.has_trailing_sep: + return self + return self._from_parsed_parts(self.drive, self.root, self._tail, False) + + @property + def _ancestors(self): + """Yields this path (sans trailing slash) and its parents.""" + yield self.without_trailing_sep() + yield from _PathParents(self) + def relative_to(self, other, /, *_deprecated, walk_up=False): """Return the relative path to another path identified by the passed arguments. If the operation is not possible (because this is not @@ -406,8 +456,8 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): other = self.with_segments(other, *_deprecated) elif not isinstance(other, PurePathBase): other = self.with_segments(other) - for step, path in enumerate(chain([other], other.parents)): - if path == self or path in self.parents: + for step, path in enumerate(other._ancestors): + if path in self._ancestors: break elif not walk_up: raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}") @@ -416,7 +466,8 @@ def relative_to(self, other, /, *_deprecated, walk_up=False): else: raise ValueError(f"{str(self)!r} and {str(other)!r} have different anchors") parts = ['..'] * step + self._tail[len(path._tail):] - return self._from_parsed_parts('', '', parts) + has_trailing_sep = self.has_trailing_sep and bool(parts) + return self._from_parsed_parts('', '', parts, has_trailing_sep) def is_relative_to(self, other, /, *_deprecated): """Return True if the path is relative to another path or False. @@ -430,7 +481,7 @@ def is_relative_to(self, other, /, *_deprecated): other = self.with_segments(other, *_deprecated) elif not isinstance(other, PurePathBase): other = self.with_segments(other) - return other == self or other in self.parents + return other.without_trailing_sep() in self._ancestors @property def parts(self): @@ -797,20 +848,24 @@ def _scandir(self): from contextlib import nullcontext return nullcontext(self.iterdir()) - def _make_child_relpath(self, name): + def _make_child_relpath(self, name, has_trailing_sep=False): path_str = str(self) tail = self._tail - if tail: - path_str = f'{path_str}{self.pathmod.sep}{name}' + sep = self.pathmod.sep + if tail and not self.has_trailing_sep: + path_str = f'{path_str}{sep}{name}' elif path_str != '.': path_str = f'{path_str}{name}' else: path_str = name + if has_trailing_sep: + path_str = f'{path_str}{sep}' path = self.with_segments(path_str) path._str = path_str path._drv = self.drive path._root = self.root path._tail_cached = tail + [name] + path._has_trailing_sep = has_trailing_sep return path def glob(self, pattern, *, case_sensitive=None, follow_symlinks=None): @@ -834,24 +889,23 @@ def _glob(self, pattern, case_sensitive, follow_symlinks): raise NotImplementedError("Non-relative patterns are unsupported") elif not path_pattern._tail: raise ValueError("Unacceptable pattern: {!r}".format(pattern)) - - pattern_parts = path_pattern._tail.copy() - 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] == '**': + elif path_pattern.name == '**' and not path_pattern.has_trailing_sep: # GH-70303: '**' only matches directories. Add trailing slash. warnings.warn( "Pattern ending '**' will match files and directories in a " "future Python release. Add a trailing slash to match only " "directories and remove this warning.", FutureWarning, 3) - pattern_parts.append('') + path_pattern = path_pattern.with_trailing_sep() if case_sensitive is None: # TODO: evaluate case-sensitivity of each directory in _select_children(). case_sensitive = _is_case_sensitive(self.pathmod) + sep = self.pathmod.sep + pattern_str = str(path_pattern) + pattern_parts = pattern_str.split(sep) + # If symlinks are handled consistently, and the pattern does not # contain '..' components, then we can use a 'walk-and-match' strategy # when expanding '**' wildcards. When a '**' wildcard is encountered, @@ -861,8 +915,12 @@ def _glob(self, pattern, case_sensitive, follow_symlinks): # do not perform any filesystem access, which can be much faster! filter_paths = follow_symlinks is not None and '..' not in pattern_parts deduplicate_paths = False - sep = self.pathmod.sep - paths = iter([self] if self.is_dir() else []) + if not self.is_dir(): + paths = iter([]) + elif not self._tail: + paths = iter([self]) + else: + paths = iter([self.with_trailing_sep()]) part_idx = 0 while part_idx < len(pattern_parts): part = pattern_parts[part_idx] @@ -871,7 +929,8 @@ def _glob(self, pattern, case_sensitive, follow_symlinks): # Trailing slash. pass elif part == '..': - paths = (path._make_child_relpath('..') for path in paths) + dir_only = part_idx < len(pattern_parts) + paths = _select_parents(paths, dir_only) elif part == '**': # Consume adjacent '**' components. while part_idx < len(pattern_parts) and pattern_parts[part_idx] == '**': @@ -883,7 +942,7 @@ def _glob(self, pattern, case_sensitive, follow_symlinks): # Filter out paths that don't match pattern. prefix_len = len(str(self._make_child_relpath('_'))) - 1 - match = _compile_pattern(str(path_pattern), sep, case_sensitive) + match = _compile_pattern(pattern_str, sep, case_sensitive) paths = (path for path in paths if match(str(path), prefix_len)) return paths diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 00cfdd37e568a6..408a90875d1f14 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -143,7 +143,7 @@ def test_as_uri_common(self): P().as_uri() def test_repr_roundtrips(self): - for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): + for pathstr in ('a', 'a/', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c', 'a/b/c/'): with self.subTest(pathstr=pathstr): p = self.cls(pathstr) r = repr(p) @@ -161,13 +161,13 @@ def test_parse_path(self): check = self._check_parse_path # Collapsing of excess leading slashes, except for the double-slash # special case. - check('//a/b', '', '//', ['a', 'b']) - check('///a/b', '', '/', ['a', 'b']) - check('////a/b', '', '/', ['a', 'b']) + check('//a/b', '', '//', ['a', 'b'], False) + check('///a/b', '', '/', ['a', 'b'], False) + check('////a/b', '', '/', ['a', 'b'], False) # Paths which look like NT paths aren't treated specially. - check('c:a', '', '', ['c:a',]) - check('c:\\a', '', '', ['c:\\a',]) - check('\\a', '', '', ['\\a',]) + check('c:a', '', '', ['c:a',], False) + check('c:\\a', '', '', ['c:\\a',], False) + check('\\a', '', '', ['\\a',], False) def test_root(self): P = self.cls @@ -254,7 +254,8 @@ class PureWindowsPathTest(PurePathTest): equivalences = PurePathTest.equivalences.copy() equivalences.update({ './a:b': [ ('./a:b',) ], - 'c:a': [ ('c:', 'a'), ('c:', 'a/'), ('.', 'c:', 'a') ], + 'c:a': [ ('c:', 'a'), ('.', 'c:', 'a') ], + 'c:a/': [('c:', 'a/'), ('c:', 'a', '.')], 'c:/a': [ ('c:/', 'a'), ('c:', '/', 'a'), ('c:', '/a'), ('/z', 'c:/', 'a'), ('//x/y', 'c:/', 'a'), @@ -268,62 +269,68 @@ class PureWindowsPathTest(PurePathTest): def test_parse_path(self): check = self._check_parse_path # First part is anchored. - check('c:', 'c:', '', []) - check('c:/', 'c:', '\\', []) - check('/', '', '\\', []) - check('c:a', 'c:', '', ['a']) - check('c:/a', 'c:', '\\', ['a']) - check('/a', '', '\\', ['a']) + check('c:', 'c:', '', [], False) + check('c:/', 'c:', '\\', [], False) + check('/', '', '\\', [], False) + check('c:a', 'c:', '', ['a'], False) + check('c:/a', 'c:', '\\', ['a'], False) + check('/a', '', '\\', ['a'], False) # UNC paths. - check('//', '\\\\', '', []) - check('//a', '\\\\a', '', []) - check('//a/', '\\\\a\\', '', []) - check('//a/b', '\\\\a\\b', '\\', []) - check('//a/b/', '\\\\a\\b', '\\', []) - check('//a/b/c', '\\\\a\\b', '\\', ['c']) + check('//', '\\\\', '', [], False) + check('//a', '\\\\a', '', [], False) + check('//a/', '\\\\a\\', '', [], False) + check('//a/b', '\\\\a\\b', '\\', [], False) + check('//a/b/', '\\\\a\\b', '\\', [], False) + check('//a/b/c', '\\\\a\\b', '\\', ['c'], False) # Collapsing and stripping excess slashes. - check('Z://b//c/d/', 'Z:', '\\', ['b', 'c', 'd']) + check('Z://b//c/d/', 'Z:', '\\', ['b', 'c', 'd'], True) # UNC paths. - check('//b/c//d', '\\\\b\\c', '\\', ['d']) + check('//b/c//d', '\\\\b\\c', '\\', ['d'], False) # Extended paths. - check('//./c:', '\\\\.\\c:', '', []) - check('//?/c:/', '\\\\?\\c:', '\\', []) - check('//?/c:/a', '\\\\?\\c:', '\\', ['a']) + check('//./c:', '\\\\.\\c:', '', [], False) + check('//?/c:/', '\\\\?\\c:', '\\', [], False) + check('//?/c:/a', '\\\\?\\c:', '\\', ['a'], False) # Extended UNC paths (format is "\\?\UNC\server\share"). - check('//?', '\\\\?', '', []) - check('//?/', '\\\\?\\', '', []) - check('//?/UNC', '\\\\?\\UNC', '', []) - check('//?/UNC/', '\\\\?\\UNC\\', '', []) - check('//?/UNC/b', '\\\\?\\UNC\\b', '', []) - check('//?/UNC/b/', '\\\\?\\UNC\\b\\', '', []) - check('//?/UNC/b/c', '\\\\?\\UNC\\b\\c', '\\', []) - check('//?/UNC/b/c/', '\\\\?\\UNC\\b\\c', '\\', []) - check('//?/UNC/b/c/d', '\\\\?\\UNC\\b\\c', '\\', ['d']) + check('//?', '\\\\?', '', [], False) + check('//?/', '\\\\?\\', '', [], False) + check('//?/UNC', '\\\\?\\UNC', '', [], False) + check('//?/UNC/', '\\\\?\\UNC\\', '', [], False) + check('//?/UNC/b', '\\\\?\\UNC\\b', '', [], False) + check('//?/UNC/b/', '\\\\?\\UNC\\b\\', '', [], False) + check('//?/UNC/b/c', '\\\\?\\UNC\\b\\c', '\\', [], False) + check('//?/UNC/b/c/', '\\\\?\\UNC\\b\\c', '\\', [], False) + check('//?/UNC/b/c/d', '\\\\?\\UNC\\b\\c', '\\', ['d'], False) # UNC device paths - check('//./BootPartition/', '\\\\.\\BootPartition', '\\', []) - check('//?/BootPartition/', '\\\\?\\BootPartition', '\\', []) - check('//./PhysicalDrive0', '\\\\.\\PhysicalDrive0', '', []) - check('//?/Volume{}/', '\\\\?\\Volume{}', '\\', []) - check('//./nul', '\\\\.\\nul', '', []) + check('//./BootPartition/', '\\\\.\\BootPartition', '\\', [], False) + check('//?/BootPartition/', '\\\\?\\BootPartition', '\\', [], False) + check('//./PhysicalDrive0', '\\\\.\\PhysicalDrive0', '', [], False) + check('//?/Volume{}/', '\\\\?\\Volume{}', '\\', [], False) + check('//./nul', '\\\\.\\nul', '', [], False) # Paths to files with NTFS alternate data streams - check('./c:s', '', '', ['c:s']) - check('cc:s', '', '', ['cc:s']) - check('C:c:s', 'C:', '', ['c:s']) - check('C:/c:s', 'C:', '\\', ['c:s']) - check('D:a/c:b', 'D:', '', ['a', 'c:b']) - check('D:/a/c:b', 'D:', '\\', ['a', 'c:b']) + check('./c:s', '', '', ['c:s'], False) + check('cc:s', '', '', ['cc:s'], False) + check('C:c:s', 'C:', '', ['c:s'], False) + check('C:/c:s', 'C:', '\\', ['c:s'], False) + check('D:a/c:b', 'D:', '', ['a', 'c:b'], False) + check('D:/a/c:b', 'D:', '\\', ['a', 'c:b'], False) def test_str(self): p = self.cls('a/b/c') self.assertEqual(str(p), 'a\\b\\c') + p = self.cls('a/b/c/') + self.assertEqual(str(p), 'a\\b\\c\\') p = self.cls('c:/a/b/c') self.assertEqual(str(p), 'c:\\a\\b\\c') + p = self.cls('c:/a/b/c/') + self.assertEqual(str(p), 'c:\\a\\b\\c\\') p = self.cls('//a/b') self.assertEqual(str(p), '\\\\a\\b\\') p = self.cls('//a/b/c') self.assertEqual(str(p), '\\\\a\\b\\c') p = self.cls('//a/b/c/d') self.assertEqual(str(p), '\\\\a\\b\\c\\d') + p = self.cls('//a/b/c/d/') + self.assertEqual(str(p), '\\\\a\\b\\c\\d\\') def test_str_subclass(self): self._check_str_subclass('.\\a:b') @@ -341,6 +348,7 @@ def test_eq(self): P = self.cls self.assertEqual(P('c:a/b'), P('c:a/b')) self.assertEqual(P('c:a/b'), P('c:', 'a', 'b')) + self.assertNotEqual(P('c:a/b'), P('c:a/b/')) self.assertNotEqual(P('c:a/b'), P('d:a/b')) self.assertNotEqual(P('c:a/b'), P('c:/a/b')) self.assertNotEqual(P('/a/b'), P('c:/a/b')) @@ -439,10 +447,30 @@ def test_parent(self): self.assertEqual(p.parent, P('//a/b/c')) self.assertEqual(p.parent.parent, P('//a/b')) self.assertEqual(p.parent.parent.parent, P('//a/b')) + # Trailing sep + p = P('z:/a/b/c/') + self.assertEqual(p.parent, P('z:/a/b')) + self.assertEqual(p.parent.parent, P('z:/a')) + self.assertEqual(p.parent.parent.parent, P('z:/')) + self.assertEqual(p.parent.parent.parent.parent, P('z:/')) def test_parents(self): # Anchored P = self.cls + p = P('z:a/b') + par = p.parents + self.assertEqual(len(par), 2) + self.assertEqual(par[0], P('z:a')) + self.assertEqual(par[1], P('z:')) + self.assertEqual(par[0:1], (P('z:a'),)) + self.assertEqual(par[:-1], (P('z:a'),)) + self.assertEqual(par[:2], (P('z:a'), P('z:'))) + self.assertEqual(par[1:], (P('z:'),)) + self.assertEqual(par[::2], (P('z:a'),)) + self.assertEqual(par[::-1], (P('z:'), P('z:a'))) + self.assertEqual(list(par), [P('z:a'), P('z:')]) + with self.assertRaises(IndexError): + par[2] p = P('z:a/b/') par = p.parents self.assertEqual(len(par), 2) @@ -455,6 +483,20 @@ def test_parents(self): self.assertEqual(par[::2], (P('z:a'),)) self.assertEqual(par[::-1], (P('z:'), P('z:a'))) self.assertEqual(list(par), [P('z:a'), P('z:')]) + with self.assertRaises(IndexError): + par[2] + p = P('z:/a/b') + par = p.parents + self.assertEqual(len(par), 2) + self.assertEqual(par[0], P('z:/a')) + self.assertEqual(par[1], P('z:/')) + self.assertEqual(par[0:1], (P('z:/a'),)) + self.assertEqual(par[0:-1], (P('z:/a'),)) + self.assertEqual(par[:2], (P('z:/a'), P('z:/'))) + self.assertEqual(par[1:], (P('z:/'),)) + self.assertEqual(par[::2], (P('z:/a'),)) + self.assertEqual(par[::-1], (P('z:/'), P('z:/a'),)) + self.assertEqual(list(par), [P('z:/a'), P('z:/')]) with self.assertRaises(IndexError): par[2] p = P('z:/a/b/') @@ -522,18 +564,23 @@ def test_name(self): self.assertEqual(P('c:').name, '') self.assertEqual(P('c:/').name, '') self.assertEqual(P('c:a/b').name, 'b') + self.assertEqual(P('c:a/b/').name, 'b') self.assertEqual(P('c:/a/b').name, 'b') + self.assertEqual(P('c:/a/b/').name, 'b') self.assertEqual(P('c:a/b.py').name, 'b.py') self.assertEqual(P('c:/a/b.py').name, 'b.py') self.assertEqual(P('//My.py/Share.php').name, '') self.assertEqual(P('//My.py/Share.php/a/b').name, 'b') + self.assertEqual(P('c:/etc/cron.d/').name, 'cron.d') def test_suffix(self): P = self.cls self.assertEqual(P('c:').suffix, '') self.assertEqual(P('c:/').suffix, '') self.assertEqual(P('c:a/b').suffix, '') + self.assertEqual(P('c:a/b/').suffix, '') self.assertEqual(P('c:/a/b').suffix, '') + self.assertEqual(P('c:/a/b/').suffix, '') self.assertEqual(P('c:a/b.py').suffix, '.py') self.assertEqual(P('c:/a/b.py').suffix, '.py') self.assertEqual(P('c:a/.hgrc').suffix, '') @@ -546,13 +593,16 @@ def test_suffix(self): self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('//My.py/Share.php').suffix, '') self.assertEqual(P('//My.py/Share.php/a/b').suffix, '') + self.assertEqual(P('c:/etc/cron.d/').suffix, '.d') def test_suffixes(self): P = self.cls self.assertEqual(P('c:').suffixes, []) self.assertEqual(P('c:/').suffixes, []) self.assertEqual(P('c:a/b').suffixes, []) + self.assertEqual(P('c:a/b/').suffixes, []) self.assertEqual(P('c:/a/b').suffixes, []) + self.assertEqual(P('c:/a/b/').suffixes, []) self.assertEqual(P('c:a/b.py').suffixes, ['.py']) self.assertEqual(P('c:/a/b.py').suffixes, ['.py']) self.assertEqual(P('c:a/.hgrc').suffixes, []) @@ -565,6 +615,7 @@ def test_suffixes(self): self.assertEqual(P('//My.py/Share.php/a/b').suffixes, []) self.assertEqual(P('c:a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('c:/a/Some name. Ending with a dot.').suffixes, []) + self.assertEqual(P('c:/etc/cron.d/').suffixes, ['.d']) def test_stem(self): P = self.cls @@ -573,12 +624,14 @@ def test_stem(self): self.assertEqual(P('c:..').stem, '..') self.assertEqual(P('c:/').stem, '') self.assertEqual(P('c:a/b').stem, 'b') + self.assertEqual(P('c:a/b/').stem, 'b') self.assertEqual(P('c:a/b.py').stem, 'b') self.assertEqual(P('c:a/.hgrc').stem, '.hgrc') self.assertEqual(P('c:a/.hg.rc').stem, '.hg') self.assertEqual(P('c:a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('c:a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') + self.assertEqual(P('c:/etc/cron.d/').stem, 'cron') def test_with_name(self): P = self.cls @@ -586,6 +639,7 @@ def test_with_name(self): self.assertEqual(P('c:/a/b').with_name('d.xml'), P('c:/a/d.xml')) self.assertEqual(P('c:a/Dot ending.').with_name('d.xml'), P('c:a/d.xml')) self.assertEqual(P('c:/a/Dot ending.').with_name('d.xml'), P('c:/a/d.xml')) + self.assertEqual(P('c:/etc/cron.d/').with_name('tron.g'), P('c:/etc/tron.g/')) self.assertRaises(ValueError, P('c:').with_name, 'd.xml') self.assertRaises(ValueError, P('c:/').with_name, 'd.xml') self.assertRaises(ValueError, P('//My/Share').with_name, 'd.xml') @@ -602,6 +656,7 @@ def test_with_stem(self): self.assertEqual(P('c:/a/b').with_stem('d'), P('c:/a/d')) self.assertEqual(P('c:a/Dot ending.').with_stem('d'), P('c:a/d')) self.assertEqual(P('c:/a/Dot ending.').with_stem('d'), P('c:/a/d')) + self.assertEqual(P('c:/etc/cron.d/').with_stem('tron'), P('c:/etc/tron.d/')) self.assertRaises(ValueError, P('c:').with_stem, 'd') self.assertRaises(ValueError, P('c:/').with_stem, 'd') self.assertRaises(ValueError, P('//My/Share').with_stem, 'd') @@ -618,6 +673,7 @@ def test_with_suffix(self): self.assertEqual(P('c:/a/b').with_suffix('.gz'), P('c:/a/b.gz')) self.assertEqual(P('c:a/b.py').with_suffix('.gz'), P('c:a/b.gz')) self.assertEqual(P('c:/a/b.py').with_suffix('.gz'), P('c:/a/b.gz')) + self.assertEqual(P('c:/etc/cron.d/').with_suffix('.g'), P('c:/etc/cron.g/')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') @@ -991,6 +1047,9 @@ def test_absolute_common(self): self.assertEqual(str(P('a', '..').absolute()), os.path.join(self.base, 'a', '..')) self.assertEqual(str(P('..', 'b').absolute()), os.path.join(self.base, '..', 'b')) + # Trailing slash should be preserved + self.assertEqual(str(P('a/').absolute()), os.path.join(self.base, 'a', '')) + def _test_home(self, p): q = self.cls(os.path.expanduser('~')) self.assertEqual(p, q) @@ -1018,6 +1077,12 @@ def test_expanduser_common(self): P = self.cls p = P('~') self.assertEqual(p.expanduser(), P(os.path.expanduser('~'))) + p = P('~/') + self.assertEqual(p.expanduser(), P(os.path.expanduser('~/'))) + p = P('~/foo') + self.assertEqual(p.expanduser(), P(os.path.expanduser('~/foo'))) + p = P('~/foo/') + self.assertEqual(p.expanduser(), P(os.path.expanduser('~/foo/'))) p = P('foo') self.assertEqual(p.expanduser(), p) p = P('/~') @@ -1797,10 +1862,12 @@ def test_absolute(self): # Relative path with root self.assertEqual(str(P('\\').absolute()), drive + '\\') self.assertEqual(str(P('\\foo').absolute()), drive + '\\foo') + self.assertEqual(str(P('\\foo\\').absolute()), drive + '\\foo\\') # Relative path on current drive self.assertEqual(str(P(drive).absolute()), self.base) self.assertEqual(str(P(drive + 'foo').absolute()), os.path.join(self.base, 'foo')) + self.assertEqual(str(P(drive + 'foo\\').absolute()), os.path.join(self.base, 'foo\\')) with os_helper.subst_drive(self.base) as other_drive: # Set the working directory on the substitute drive @@ -1812,6 +1879,7 @@ def test_absolute(self): # Relative path on another drive self.assertEqual(str(P(other_drive).absolute()), other_cwd) self.assertEqual(str(P(other_drive + 'foo').absolute()), other_cwd + '\\foo') + self.assertEqual(str(P(other_drive + 'foo\\').absolute()), other_cwd + '\\foo\\') def test_glob(self): P = self.cls diff --git a/Lib/test/test_pathlib/test_pathlib_abc.py b/Lib/test/test_pathlib/test_pathlib_abc.py index a272973d9c1d61..6d430bbf3f0e7c 100644 --- a/Lib/test/test_pathlib/test_pathlib_abc.py +++ b/Lib/test/test_pathlib/test_pathlib_abc.py @@ -59,11 +59,13 @@ class DummyPurePathTest(unittest.TestCase): # supposed to produce equal paths. equivalences = { 'a/b': [ - ('a', 'b'), ('a/', 'b'), ('a', 'b/'), ('a/', 'b/'), - ('a/b/',), ('a//b',), ('a//b//',), + ('a', 'b'), ('a/', 'b'), ('a//b',), # Empty components get removed. ('', 'a', 'b'), ('a', '', 'b'), ('a', 'b', ''), ], + 'a/b/': [ + ('a', 'b/'), ('a/', 'b/'), ('a/b/',), ('a//b//',), + ], '/b/c/d': [ ('a', '/b/c', 'd'), ('/a', '/b/c', 'd'), # Empty components get removed. @@ -168,19 +170,19 @@ def _check_parse_path(self, raw_path, *expected): def test_parse_path_common(self): check = self._check_parse_path sep = self.pathmod.sep - check('', '', '', []) - check('a', '', '', ['a']) - check('a/', '', '', ['a']) - check('a/b', '', '', ['a', 'b']) - check('a/b/', '', '', ['a', 'b']) - check('a/b/c/d', '', '', ['a', 'b', 'c', 'd']) - check('a/b//c/d', '', '', ['a', 'b', 'c', 'd']) - check('a/b/c/d', '', '', ['a', 'b', 'c', 'd']) - check('.', '', '', []) - check('././b', '', '', ['b']) - check('a/./b', '', '', ['a', 'b']) - check('a/./.', '', '', ['a']) - check('/a/b', '', sep, ['a', 'b']) + check('', '', '', [], False) + check('a', '', '', ['a'], False) + check('a/', '', '', ['a'], True) + check('a/b', '', '', ['a', 'b'], False) + check('a/b/', '', '', ['a', 'b'], True) + check('a/b/c/d', '', '', ['a', 'b', 'c', 'd'], False) + check('a/b//c/d', '', '', ['a', 'b', 'c', 'd'], False) + check('a/b/c/d', '', '', ['a', 'b', 'c', 'd'], False) + check('.', '', '', [], False) + check('././b', '', '', ['b'], False) + check('a/./b', '', '', ['a', 'b'], False) + check('a/./.', '', '', ['a'], True) + check('/a/b', '', sep, ['a', 'b'], False) def test_join_common(self): P = self.cls @@ -215,7 +217,7 @@ def _check_str(self, expected, args): def test_str_common(self): # Canonicalized paths roundtrip. - for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): + for pathstr in ('a', 'a/', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c', '/a/b/c/'): self._check_str(pathstr, (pathstr,)) # Special case for the empty path. self._check_str('.', ('',)) @@ -223,12 +225,12 @@ def test_str_common(self): def test_as_posix_common(self): P = self.cls - for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): + for pathstr in ('a', 'a/', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c', '/a/b/c/'): self.assertEqual(P(pathstr).as_posix(), pathstr) # Other tests for as_posix() are in test_equivalences(). def test_repr_common(self): - for pathstr in ('a', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c'): + for pathstr in ('a', 'a/', 'a/b', 'a/b/c', '/', '/a/b', '/a/b/c', '/a/b/c/'): with self.subTest(pathstr=pathstr): p = self.cls(pathstr) clsname = p.__class__.__name__ @@ -244,9 +246,12 @@ def test_eq_common(self): self.assertEqual(P('a/b'), P('a/b')) self.assertEqual(P('a/b'), P('a', 'b')) self.assertNotEqual(P('a/b'), P('a')) + self.assertNotEqual(P('a/b'), P('a/')) + self.assertNotEqual(P('a/b'), P('a/b/')) self.assertNotEqual(P('a/b'), P('/a/b')) self.assertNotEqual(P('a/b'), P()) self.assertNotEqual(P('/a/b'), P('/')) + self.assertNotEqual(P('/a/b'), P('/a/b/')) self.assertNotEqual(P(), P('/')) self.assertNotEqual(P(), "") self.assertNotEqual(P(), {}) @@ -292,7 +297,6 @@ def test_match_common(self): self.assertTrue(P('a/b/c.py').match('**')) self.assertTrue(P('/a/b/c.py').match('**')) self.assertTrue(P('/a/b/c.py').match('/**')) - self.assertTrue(P('/a/b/c.py').match('**/')) self.assertTrue(P('/a/b/c.py').match('/a/**')) self.assertTrue(P('/a/b/c.py').match('**/*.py')) self.assertTrue(P('/a/b/c.py').match('/**/*.py')) @@ -320,6 +324,33 @@ def test_match_common(self): self.assertFalse(P().match('*')) self.assertTrue(P().match('**')) self.assertFalse(P().match('**/*')) + # Matching empty path with ** and **/ + self.assertTrue(P().match('**')) + self.assertTrue(P().match('**/')) + self.assertTrue(P('').match('**')) + self.assertTrue(P('').match('**/')) + self.assertTrue(P('.').match('**')) + self.assertTrue(P('.').match('**/')) + self.assertTrue(P('', '').match('**')) + self.assertTrue(P('', '').match('**/')) + self.assertTrue(P('.', '').match('**')) + self.assertTrue(P('.', '').match('**/')) + self.assertTrue(P('', '.').match('**')) + self.assertTrue(P('', '.').match('**/')) + self.assertTrue(P('.', '.').match('**')) + self.assertTrue(P('.', '.').match('**/')) + # Matching single segment with/without trailing slash + self.assertTrue(P('foo').match('*')) + self.assertFalse(P('foo').match('*/')) + self.assertFalse(P('foo/').match('*')) + self.assertTrue(P('foo/').match('*/')) + # Matching any segments with trailing slash + self.assertFalse(P('/foo').match('**/')) + self.assertTrue(P('/foo/').match('**/')) + self.assertFalse(P('foo').match('**/')) + self.assertTrue(P('foo/').match('**/')) + self.assertFalse(P('foo/bar').match('**/')) + self.assertTrue(P('foo/bar/').match('**/')) def test_parts_common(self): # `parts` returns a tuple. @@ -365,6 +396,12 @@ def test_parent_common(self): self.assertEqual(p.parent.parent, P('/a')) self.assertEqual(p.parent.parent.parent, P('/')) self.assertEqual(p.parent.parent.parent.parent, P('/')) + # Trailing sep + p = P('/a/b/c/') + self.assertEqual(p.parent, P('/a/b')) + self.assertEqual(p.parent.parent, P('/a')) + self.assertEqual(p.parent.parent.parent, P('/')) + self.assertEqual(p.parent.parent.parent.parent, P('/')) def test_parents_common(self): # Relative @@ -412,6 +449,27 @@ def test_parents_common(self): par[-4] with self.assertRaises(IndexError): par[3] + # Trailing sep + p = P('/a/b/c/') + par = p.parents + self.assertEqual(len(par), 3) + self.assertEqual(par[0], P('/a/b')) + self.assertEqual(par[1], P('/a')) + self.assertEqual(par[2], P('/')) + self.assertEqual(par[-1], P('/')) + self.assertEqual(par[-2], P('/a')) + self.assertEqual(par[-3], P('/a/b')) + self.assertEqual(par[0:1], (P('/a/b'),)) + self.assertEqual(par[:2], (P('/a/b'), P('/a'))) + self.assertEqual(par[:-1], (P('/a/b'), P('/a'))) + self.assertEqual(par[1:], (P('/a'), P('/'))) + self.assertEqual(par[::2], (P('/a/b'), P('/'))) + self.assertEqual(par[::-1], (P('/'), P('/a'), P('/a/b'))) + self.assertEqual(list(par), [P('/a/b'), P('/a'), P('/')]) + with self.assertRaises(IndexError): + par[-4] + with self.assertRaises(IndexError): + par[3] def test_drive_common(self): P = self.cls @@ -441,10 +499,13 @@ def test_name_common(self): self.assertEqual(P('.').name, '') self.assertEqual(P('/').name, '') self.assertEqual(P('a/b').name, 'b') + self.assertEqual(P('a/b/').name, 'b') self.assertEqual(P('/a/b').name, 'b') + self.assertEqual(P('/a/b/').name, 'b') self.assertEqual(P('/a/b/.').name, 'b') self.assertEqual(P('a/b.py').name, 'b.py') self.assertEqual(P('/a/b.py').name, 'b.py') + self.assertEqual(P('/etc/cron.d/').name, 'cron.d') def test_suffix_common(self): P = self.cls @@ -453,7 +514,9 @@ def test_suffix_common(self): self.assertEqual(P('..').suffix, '') self.assertEqual(P('/').suffix, '') self.assertEqual(P('a/b').suffix, '') + self.assertEqual(P('a/b/').suffix, '') self.assertEqual(P('/a/b').suffix, '') + self.assertEqual(P('/a/b/').suffix, '') self.assertEqual(P('/a/b/.').suffix, '') self.assertEqual(P('a/b.py').suffix, '.py') self.assertEqual(P('/a/b.py').suffix, '.py') @@ -465,6 +528,7 @@ def test_suffix_common(self): self.assertEqual(P('/a/b.tar.gz').suffix, '.gz') self.assertEqual(P('a/Some name. Ending with a dot.').suffix, '') self.assertEqual(P('/a/Some name. Ending with a dot.').suffix, '') + self.assertEqual(P('/etc/cron.d/').suffix, '.d') def test_suffixes_common(self): P = self.cls @@ -472,7 +536,9 @@ def test_suffixes_common(self): self.assertEqual(P('.').suffixes, []) self.assertEqual(P('/').suffixes, []) self.assertEqual(P('a/b').suffixes, []) + self.assertEqual(P('a/b/').suffixes, []) self.assertEqual(P('/a/b').suffixes, []) + self.assertEqual(P('/a/b/').suffixes, []) self.assertEqual(P('/a/b/.').suffixes, []) self.assertEqual(P('a/b.py').suffixes, ['.py']) self.assertEqual(P('/a/b.py').suffixes, ['.py']) @@ -484,6 +550,7 @@ def test_suffixes_common(self): self.assertEqual(P('/a/b.tar.gz').suffixes, ['.tar', '.gz']) self.assertEqual(P('a/Some name. Ending with a dot.').suffixes, []) self.assertEqual(P('/a/Some name. Ending with a dot.').suffixes, []) + self.assertEqual(P('/etc/cron.d/').suffixes, ['.d']) def test_stem_common(self): P = self.cls @@ -492,12 +559,14 @@ def test_stem_common(self): self.assertEqual(P('..').stem, '..') self.assertEqual(P('/').stem, '') self.assertEqual(P('a/b').stem, 'b') + self.assertEqual(P('a/b/').stem, 'b') self.assertEqual(P('a/b.py').stem, 'b') self.assertEqual(P('a/.hgrc').stem, '.hgrc') self.assertEqual(P('a/.hg.rc').stem, '.hg') self.assertEqual(P('a/b.tar.gz').stem, 'b.tar') self.assertEqual(P('a/Some name. Ending with a dot.').stem, 'Some name. Ending with a dot.') + self.assertEqual(P('/etc/cron.d/').stem, 'cron') def test_with_name_common(self): P = self.cls @@ -507,6 +576,7 @@ def test_with_name_common(self): self.assertEqual(P('/a/b.py').with_name('d.xml'), P('/a/d.xml')) self.assertEqual(P('a/Dot ending.').with_name('d.xml'), P('a/d.xml')) self.assertEqual(P('/a/Dot ending.').with_name('d.xml'), P('/a/d.xml')) + self.assertEqual(P('/etc/cron.d/').with_name('tron.g'), P('/etc/tron.g/')) self.assertRaises(ValueError, P('').with_name, 'd.xml') self.assertRaises(ValueError, P('.').with_name, 'd.xml') self.assertRaises(ValueError, P('/').with_name, 'd.xml') @@ -525,6 +595,7 @@ def test_with_stem_common(self): self.assertEqual(P('/a/b.tar.gz').with_stem('d'), P('/a/d.gz')) self.assertEqual(P('a/Dot ending.').with_stem('d'), P('a/d')) self.assertEqual(P('/a/Dot ending.').with_stem('d'), P('/a/d')) + self.assertEqual(P('/etc/cron.d/').with_stem('tron'), P('/etc/tron.d/')) self.assertRaises(ValueError, P('').with_stem, 'd') self.assertRaises(ValueError, P('.').with_stem, 'd') self.assertRaises(ValueError, P('/').with_stem, 'd') @@ -540,9 +611,11 @@ def test_with_suffix_common(self): self.assertEqual(P('/a/b').with_suffix('.gz'), P('/a/b.gz')) self.assertEqual(P('a/b.py').with_suffix('.gz'), P('a/b.gz')) self.assertEqual(P('/a/b.py').with_suffix('.gz'), P('/a/b.gz')) + self.assertEqual(P('/etc/cron.d/').with_suffix('.g'), P('/etc/cron.g/')) # Stripping suffix. self.assertEqual(P('a/b.py').with_suffix(''), P('a/b')) self.assertEqual(P('/a/b').with_suffix(''), P('/a/b')) + self.assertEqual(P('/etc/cron.d/').with_suffix(''), P('/etc/cron/')) # Path doesn't have a "filename" component. self.assertRaises(ValueError, P('').with_suffix, '.gz') self.assertRaises(ValueError, P('.').with_suffix, '.gz') @@ -557,6 +630,51 @@ def test_with_suffix_common(self): self.assertRaises(ValueError, P('a/b').with_suffix, './.d') self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.') + def test_has_trailing_sep(self): + P = self.cls + self.assertFalse(P().has_trailing_sep) + self.assertFalse(P('').has_trailing_sep) + self.assertFalse(P('.').has_trailing_sep) + self.assertFalse(P('a').has_trailing_sep) + self.assertTrue(P('a/').has_trailing_sep) + self.assertFalse(P('a/b').has_trailing_sep) + self.assertTrue(P('a/b/').has_trailing_sep) + self.assertFalse(P('/').has_trailing_sep) + self.assertFalse(P('/a').has_trailing_sep) + self.assertTrue(P('/a/').has_trailing_sep) + self.assertFalse(P('/a/b').has_trailing_sep) + self.assertTrue(P('/a/b/').has_trailing_sep) + + def test_with_trailing_sep(self): + P = self.cls + self.assertRaises(ValueError, P().with_trailing_sep) + self.assertRaises(ValueError, P('').with_trailing_sep) + self.assertRaises(ValueError, P('.').with_trailing_sep) + self.assertEqual(P('a/'), P('a').with_trailing_sep()) + self.assertEqual(P('a/'), P('a/').with_trailing_sep()) + self.assertEqual(P('a/b/'), P('a/b').with_trailing_sep()) + self.assertEqual(P('a/b/'), P('a/b/').with_trailing_sep()) + self.assertRaises(ValueError, P('/').with_trailing_sep) + self.assertEqual(P('/a/'), P('/a').with_trailing_sep()) + self.assertEqual(P('/a/'), P('/a/').with_trailing_sep()) + self.assertEqual(P('/a/b/'), P('/a/b').with_trailing_sep()) + self.assertEqual(P('/a/b/'), P('/a/b/').with_trailing_sep()) + + def test_without_trailing_sep(self): + P = self.cls + self.assertEqual(P(), P().without_trailing_sep()) + self.assertEqual(P(), P('').without_trailing_sep()) + self.assertEqual(P(), P('.').without_trailing_sep()) + self.assertEqual(P('a'), P('a').without_trailing_sep()) + self.assertEqual(P('a'), P('a/').without_trailing_sep()) + self.assertEqual(P('a/b'), P('a/b').without_trailing_sep()) + self.assertEqual(P('a/b'), P('a/b/').without_trailing_sep()) + self.assertEqual(P('/'), P('/').without_trailing_sep()) + self.assertEqual(P('/a'), P('/a').without_trailing_sep()) + self.assertEqual(P('/a'), P('/a/').without_trailing_sep()) + self.assertEqual(P('/a/b'), P('/a/b').without_trailing_sep()) + self.assertEqual(P('/a/b'), P('/a/b/').without_trailing_sep()) + def test_relative_to_common(self): P = self.cls p = P('a/b') @@ -636,6 +754,25 @@ def test_relative_to_common(self): self.assertRaises(ValueError, p.relative_to, P("a/.."), walk_up=True) self.assertRaises(ValueError, p.relative_to, P("/a/.."), walk_up=True) + def test_relative_to_trailing_sep(self): + P = self.cls + self.assertEqual(P('foo').relative_to('foo'), P()) + self.assertEqual(P('foo').relative_to('foo/'), P()) + self.assertEqual(P('foo/').relative_to('foo'), P()) + self.assertEqual(P('foo/').relative_to('foo/'), P()) + self.assertEqual(P('foo/bar').relative_to('foo'), P('bar')) + self.assertEqual(P('foo/bar').relative_to('foo/'), P('bar')) + self.assertEqual(P('foo/bar/').relative_to('foo'), P('bar/')) + self.assertEqual(P('foo/bar/').relative_to('foo/'), P('bar/')) + self.assertEqual(P('foo').relative_to('foo/bar', walk_up=True), P('..')) + self.assertEqual(P('foo').relative_to('foo/bar/', walk_up=True), P('..')) + self.assertEqual(P('foo/').relative_to('foo/bar', walk_up=True), P('../')) + self.assertEqual(P('foo/').relative_to('foo/bar/', walk_up=True), P('../')) + self.assertEqual(P('foo/oof').relative_to('foo/bar', walk_up=True), P('../oof')) + self.assertEqual(P('foo/oof').relative_to('foo/bar/', walk_up=True), P('../oof')) + self.assertEqual(P('foo/oof/').relative_to('foo/bar', walk_up=True), P('../oof/')) + self.assertEqual(P('foo/oof/').relative_to('foo/bar/', walk_up=True), P('../oof/')) + def test_is_relative_to_common(self): P = self.cls p = P('a/b') @@ -671,6 +808,25 @@ def test_is_relative_to_common(self): self.assertFalse(p.is_relative_to('')) self.assertFalse(p.is_relative_to(P('a'))) + def test_is_relative_to_trailing_sep(self): + P = self.cls + self.assertTrue(P('foo').is_relative_to('foo')) + self.assertTrue(P('foo').is_relative_to('foo/')) + self.assertTrue(P('foo/').is_relative_to('foo')) + self.assertTrue(P('foo/').is_relative_to('foo/')) + self.assertTrue(P('foo/bar').is_relative_to('foo')) + self.assertTrue(P('foo/bar').is_relative_to('foo/')) + self.assertTrue(P('foo/bar/').is_relative_to('foo')) + self.assertTrue(P('foo/bar/').is_relative_to('foo/')) + self.assertFalse(P('foo').is_relative_to('foo/bar')) + self.assertFalse(P('foo').is_relative_to('foo/bar/')) + self.assertFalse(P('foo/').is_relative_to('foo/bar')) + self.assertFalse(P('foo/').is_relative_to('foo/bar/')) + self.assertFalse(P('foo/oof').is_relative_to('foo/bar')) + self.assertFalse(P('foo/oof').is_relative_to('foo/bar/')) + self.assertFalse(P('foo/oof/').is_relative_to('foo/bar')) + self.assertFalse(P('foo/oof/').is_relative_to('foo/bar/')) + # # Tests for the virtual classes.