Update pathlib, glob, fnmatch from 3.14.3

This commit is contained in:
ShaharNaveh authored and Jeong, YunWon committed 2026-02-13 22:15:19 +09:00
1 parent 85e146e868
commit 32ef23feb4
21 files changed
+6716 -4688

No files matched your search

+66 -49
View File
@@ -9,12 +9,15 @@ expression. They cache the compiled regular expressions for speed.
The function translate(PATTERN) returns a regular expression
corresponding to PATTERN. (It does not compile it.)
"""
import functools
import itertools
import os
import posixpath
import re
import functools
__all__ = ["filter", "fnmatch", "fnmatchcase", "translate"]
__all__ = ["filter", "filterfalse", "fnmatch", "fnmatchcase", "translate"]
def fnmatch(name, pat):
"""Test whether FILENAME matches PATTERN.
@@ -35,6 +38,7 @@ def fnmatch(name, pat):
pat = os.path.normcase(pat)
return fnmatchcase(name, pat)
@functools.lru_cache(maxsize=32768, typed=True)
def _compile_pattern(pat):
if isinstance(pat, bytes):
@@ -45,6 +49,7 @@ def _compile_pattern(pat):
res = translate(pat)
return re.compile(res).match
def filter(names, pat):
"""Construct a list from those elements of the iterable NAMES that match PAT."""
result = []
@@ -61,6 +66,22 @@ def filter(names, pat):
result.append(name)
return result
def filterfalse(names, pat):
"""Construct a list from those elements of the iterable NAMES that do not match PAT."""
pat = os.path.normcase(pat)
match = _compile_pattern(pat)
if os.path is posixpath:
# normcase on posix is NOP. Optimize it away from the loop.
return list(itertools.filterfalse(match, names))
result = []
for name in names:
if match(os.path.normcase(name)) is None:
result.append(name)
return result
def fnmatchcase(name, pat):
"""Test whether FILENAME matches PATTERN, including case.
@@ -77,24 +98,32 @@ def translate(pat):
There is no way to quote meta-characters.
"""
STAR = object()
parts = _translate(pat, STAR, '.')
return _join_translated_parts(parts, STAR)
parts, star_indices = _translate(pat, '*', '.')
return _join_translated_parts(parts, star_indices)
def _translate(pat, STAR, QUESTION_MARK):
_re_setops_sub = re.compile(r'([&~|])').sub
_re_escape = functools.lru_cache(maxsize=512)(re.escape)
def _translate(pat, star, question_mark):
res = []
add = res.append
star_indices = []
i, n = 0, len(pat)
while i < n:
c = pat[i]
i = i+1
if c == '*':
# store the position of the wildcard
star_indices.append(len(res))
add(star)
# compress consecutive `*` into one
if (not res) or res[-1] is not STAR:
add(STAR)
while i < n and pat[i] == '*':
i += 1
elif c == '?':
add(QUESTION_MARK)
add(question_mark)
elif c == '[':
j = i
if j < n and pat[j] == '!':
@@ -133,8 +162,6 @@ def _translate(pat, STAR, QUESTION_MARK):
# Hyphens that create ranges shouldn't be escaped.
stuff = '-'.join(s.replace('\\', r'\\').replace('-', r'\-')
for s in chunks)
# Escape set operations (&&, ~~ and ||).
stuff = re.sub(r'([&~|])', r'\\\1', stuff)
i = j+1
if not stuff:
# Empty range: never match.
@@ -143,50 +170,40 @@ def _translate(pat, STAR, QUESTION_MARK):
# Negated empty range: match any character.
add('.')
else:
# Escape set operations (&&, ~~ and ||).
stuff = _re_setops_sub(r'\\\1', stuff)
if stuff[0] == '!':
stuff = '^' + stuff[1:]
elif stuff[0] in ('^', '['):
stuff = '\\' + stuff
add(f'[{stuff}]')
else:
add(re.escape(c))
add(_re_escape(c))
assert i == n
return res
return res, star_indices
def _join_translated_parts(inp, STAR):
# Deal with STARs.
res = []
add = res.append
i, n = 0, len(inp)
# Fixed pieces at the start?
while i < n and inp[i] is not STAR:
add(inp[i])
i += 1
# Now deal with STAR fixed STAR fixed ...
# For an interior `STAR fixed` pairing, we want to do a minimal
# .*? match followed by `fixed`, with no possibility of backtracking.
# Atomic groups ("(?>...)") allow us to spell that directly.
# Note: people rely on the undocumented ability to join multiple
# translate() results together via "|" to build large regexps matching
# "one of many" shell patterns.
while i < n:
assert inp[i] is STAR
i += 1
if i == n:
add(".*")
break
assert inp[i] is not STAR
fixed = []
while i < n and inp[i] is not STAR:
fixed.append(inp[i])
i += 1
fixed = "".join(fixed)
if i == n:
add(".*")
add(fixed)
else:
add(f"(?>.*?{fixed})")
assert i == n
res = "".join(res)
return fr'(?s:{res})\Z'
def _join_translated_parts(parts, star_indices):
if not star_indices:
return fr'(?s:{"".join(parts)})\z'
iter_star_indices = iter(star_indices)
j = next(iter_star_indices)
buffer = parts[:j] # fixed pieces at the start
append, extend = buffer.append, buffer.extend
i = j + 1
for j in iter_star_indices:
# Now deal with STAR fixed STAR fixed ...
# For an interior `STAR fixed` pairing, we want to do a minimal
# .*? match followed by `fixed`, with no possibility of backtracking.
# Atomic groups ("(?>...)") allow us to spell that directly.
# Note: people rely on the undocumented ability to join multiple
# translate() results together via "|" to build large regexps matching
# "one of many" shell patterns.
append('(?>.*?')
extend(parts[i:j])
append(')')
i = j + 1
append('.*')
extend(parts[i:])
res = ''.join(buffer)
return fr'(?s:{res})\z'
Vendored
+72 -64
View File
@@ -22,6 +22,9 @@ def glob(pathname, *, root_dir=None, dir_fd=None, recursive=False,
dot are special cases that are not matched by '*' and '?'
patterns by default.
The order of the returned list is undefined. Sort it if you need a
particular order.
If `include_hidden` is true, the patterns '*', '?', '**' will match hidden
directories.
@@ -40,6 +43,9 @@ def iglob(pathname, *, root_dir=None, dir_fd=None, recursive=False,
dot are special cases that are not matched by '*' and '?'
patterns.
The order of the returned paths is undefined. Sort them if you need a
particular order.
If recursive is true, the pattern '**' will match any files and
zero or more directories and subdirectories.
"""
@@ -312,24 +318,24 @@ def translate(pat, *, recursive=False, include_hidden=False, seps=None):
if part:
if not include_hidden and part[0] in '*?':
results.append(r'(?!\.)')
results.extend(fnmatch._translate(part, f'{not_sep}*', not_sep))
results.extend(fnmatch._translate(part, f'{not_sep}*', not_sep)[0])
if idx < last_part_idx:
results.append(any_sep)
res = ''.join(results)
return fr'(?s:{res})\Z'
return fr'(?s:{res})\z'
@functools.lru_cache(maxsize=512)
def _compile_pattern(pat, sep, case_sensitive, recursive=True):
def _compile_pattern(pat, seps, case_sensitive, recursive=True):
"""Compile given glob pattern to a re.Pattern object (observing case
sensitivity)."""
flags = re.NOFLAG if case_sensitive else re.IGNORECASE
regex = translate(pat, recursive=recursive, include_hidden=True, seps=sep)
regex = translate(pat, recursive=recursive, include_hidden=True, seps=seps)
return re.compile(regex, flags=flags).match
class _Globber:
"""Class providing shell-style pattern matching and globbing.
class _GlobberBase:
"""Abstract class providing shell-style pattern matching and globbing.
"""
def __init__(self, sep, case_sensitive, case_pedantic=False, recursive=False):
@@ -338,34 +344,31 @@ class _Globber:
self.case_pedantic = case_pedantic
self.recursive = recursive
# Low-level methods
# Abstract methods
lstat = operator.methodcaller('lstat')
add_slash = operator.methodcaller('joinpath', '')
@staticmethod
def lexists(path):
"""Implements os.path.lexists().
"""
raise NotImplementedError
@staticmethod
def scandir(path):
"""Emulates os.scandir(), which returns an object that can be used as
a context manager. This method is called by walk() and glob().
"""Like os.scandir(), but generates (entry, name, path) tuples.
"""
return contextlib.nullcontext(path.iterdir())
raise NotImplementedError
@staticmethod
def concat_path(path, text):
"""Appends text to the given path.
"""Implements path concatenation.
"""
return path.with_segments(path._raw_path + text)
@staticmethod
def parse_entry(entry):
"""Returns the path of an entry yielded from scandir().
"""
return entry
raise NotImplementedError
# High-level methods
def compile(self, pat):
return _compile_pattern(pat, self.sep, self.case_sensitive, self.recursive)
def compile(self, pat, altsep=None):
seps = (self.sep, altsep) if altsep else self.sep
return _compile_pattern(pat, seps, self.case_sensitive, self.recursive)
def selector(self, parts):
"""Returns a function that selects from a given path, walking and
@@ -387,10 +390,12 @@ class _Globber:
def special_selector(self, part, parts):
"""Returns a function that selects special children of the given path.
"""
if parts:
part += self.sep
select_next = self.selector(parts)
def select_special(path, exists=False):
path = self.concat_path(self.add_slash(path), part)
path = self.concat_path(path, part)
return select_next(path, exists)
return select_special
@@ -400,14 +405,16 @@ class _Globber:
# Optimization: consume and join any subsequent literal parts here,
# rather than leaving them for the next selector. This reduces the
# number of string concatenation operations and calls to add_slash().
# number of string concatenation operations.
while parts and magic_check.search(parts[-1]) is None:
part += self.sep + parts.pop()
if parts:
part += self.sep
select_next = self.selector(parts)
def select_literal(path, exists=False):
path = self.concat_path(self.add_slash(path), part)
path = self.concat_path(path, part)
return select_next(path, exists=False)
return select_literal
@@ -423,23 +430,19 @@ class _Globber:
def select_wildcard(path, exists=False):
try:
# We must close the scandir() object before proceeding to
# avoid exhausting file descriptors when globbing deep trees.
with self.scandir(path) as scandir_it:
entries = list(scandir_it)
entries = self.scandir(path)
except OSError:
pass
else:
for entry in entries:
if match is None or match(entry.name):
for entry, entry_name, entry_path in entries:
if match is None or match(entry_name):
if dir_only:
try:
if not entry.is_dir():
continue
except OSError:
continue
entry_path = self.parse_entry(entry)
if dir_only:
entry_path = self.concat_path(entry_path, self.sep)
yield from select_next(entry_path, exists=True)
else:
yield entry_path
@@ -469,7 +472,6 @@ class _Globber:
select_next = self.selector(parts)
def select_recursive(path, exists=False):
path = self.add_slash(path)
match_pos = len(str(path))
if match is None or match(str(path), match_pos):
yield from select_next(path, exists)
@@ -480,14 +482,11 @@ class _Globber:
def select_recursive_step(stack, match_pos):
path = stack.pop()
try:
# We must close the scandir() object before proceeding to
# avoid exhausting file descriptors when globbing deep trees.
with self.scandir(path) as scandir_it:
entries = list(scandir_it)
entries = self.scandir(path)
except OSError:
pass
else:
for entry in entries:
for entry, _entry_name, entry_path in entries:
is_dir = False
try:
if entry.is_dir(follow_symlinks=follow_symlinks):
@@ -496,8 +495,10 @@ class _Globber:
pass
if is_dir or not dir_only:
entry_path = self.parse_entry(entry)
if match is None or match(str(entry_path), match_pos):
entry_path_str = str(entry_path)
if dir_only:
entry_path = self.concat_path(entry_path, self.sep)
if match is None or match(entry_path_str, match_pos):
if dir_only:
yield from select_next(entry_path, exists=True)
else:
@@ -516,30 +517,37 @@ class _Globber:
# Optimization: this path is already known to exist, e.g. because
# it was returned from os.scandir(), so we skip calling lstat().
yield path
else:
try:
self.lstat(path)
yield path
except OSError:
pass
elif self.lexists(path):
yield path
class _StringGlobber(_Globber):
lstat = staticmethod(os.lstat)
scandir = staticmethod(os.scandir)
parse_entry = operator.attrgetter('path')
class _StringGlobber(_GlobberBase):
"""Provides shell-style pattern matching and globbing for string paths.
"""
lexists = staticmethod(os.path.lexists)
concat_path = operator.add
if os.name == 'nt':
@staticmethod
def add_slash(pathname):
tail = os.path.splitroot(pathname)[2]
if not tail or tail[-1] in '\\/':
return pathname
return f'{pathname}\\'
else:
@staticmethod
def add_slash(pathname):
if not pathname or pathname[-1] == '/':
return pathname
return f'{pathname}/'
@staticmethod
def scandir(path):
# We must close the scandir() object before proceeding to
# avoid exhausting file descriptors when globbing deep trees.
with os.scandir(path) as scandir_it:
entries = list(scandir_it)
return ((entry, entry.name, entry.path) for entry in entries)
class _PathGlobber(_GlobberBase):
"""Provides shell-style pattern matching and globbing for pathlib paths.
"""
@staticmethod
def lexists(path):
return path.info.exists(follow_symlinks=False)
@staticmethod
def scandir(path):
return ((child.info, child.name, child) for child in path.iterdir())
@staticmethod
def concat_path(path, text):
return path.with_segments(str(path) + text)
+1299 -4
View File
File diff suppressed because it is too large. Load diff
-930
View File
@@ -1,930 +0,0 @@
"""
Abstract base classes for rich path objects.
This module is published as a PyPI package called "pathlib-abc".
This module is also a *PRIVATE* part of the Python standard library, where
it's developed alongside pathlib. If it finds success and maturity as a PyPI
package, it could become a public part of the standard library.
Two base classes are defined here -- PurePathBase and PathBase -- that
resemble pathlib's PurePath and Path respectively.
"""
import functools
from glob import _Globber, _no_recurse_symlinks
from errno import ENOENT, ENOTDIR, EBADF, ELOOP, EINVAL
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
__all__ = ["UnsupportedOperation"]
#
# Internals
#
_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
# EBADF - guard against macOS `stat` throwing EBADF
_IGNORED_ERRNOS = (ENOENT, ENOTDIR, EBADF, ELOOP)
_IGNORED_WINERRORS = (
_WINERROR_NOT_READY,
_WINERROR_INVALID_NAME,
_WINERROR_CANT_RESOLVE_FILENAME)
def _ignore_error(exception):
return (getattr(exception, 'errno', None) in _IGNORED_ERRNOS or
getattr(exception, 'winerror', None) in _IGNORED_WINERRORS)
@functools.cache
def _is_case_sensitive(parser):
return parser.normcase('Aa') == 'Aa'
class UnsupportedOperation(NotImplementedError):
"""An exception that is raised when an unsupported operation is called on
a path object.
"""
pass
class ParserBase:
"""Base class for path parsers, which do low-level path manipulation.
Path parsers provide a subset of the os.path API, specifically those
functions needed to provide PurePathBase functionality. Each PurePathBase
subclass references its path parser via a 'parser' class attribute.
Every method in this base class raises an UnsupportedOperation exception.
"""
@classmethod
def _unsupported_msg(cls, attribute):
return f"{cls.__name__}.{attribute} is unsupported"
@property
def sep(self):
"""The character used to separate path components."""
raise UnsupportedOperation(self._unsupported_msg('sep'))
def join(self, path, *paths):
"""Join path segments."""
raise UnsupportedOperation(self._unsupported_msg('join()'))
def split(self, path):
"""Split the path into a pair (head, tail), where *head* is everything
before the final path separator, and *tail* is everything after.
Either part may be empty.
"""
raise UnsupportedOperation(self._unsupported_msg('split()'))
def splitdrive(self, path):
"""Split the path into a 2-item tuple (drive, tail), where *drive* is
a device name or mount point, and *tail* is everything after the
drive. Either part may be empty."""
raise UnsupportedOperation(self._unsupported_msg('splitdrive()'))
def normcase(self, path):
"""Normalize the case of the path."""
raise UnsupportedOperation(self._unsupported_msg('normcase()'))
def isabs(self, path):
"""Returns whether the path is absolute, i.e. unaffected by the
current directory or drive."""
raise UnsupportedOperation(self._unsupported_msg('isabs()'))
class PurePathBase:
"""Base class for pure path objects.
This class *does not* provide several magic methods that are defined in
its subclass PurePath. They are: __fspath__, __bytes__, __reduce__,
__hash__, __eq__, __lt__, __le__, __gt__, __ge__. Its initializer and path
joining methods accept only strings, not os.PathLike objects more broadly.
"""
__slots__ = (
# The `_raw_path` slot store a joined string path. This is set in the
# `__init__()` method.
'_raw_path',
# The '_resolving' slot stores a boolean indicating whether the path
# is being processed by `PathBase.resolve()`. This prevents duplicate
# work from occurring when `resolve()` calls `stat()` or `readlink()`.
'_resolving',
)
parser = ParserBase()
_globber = _Globber
def __init__(self, path, *paths):
self._raw_path = self.parser.join(path, *paths) if paths else path
if not isinstance(self._raw_path, str):
raise TypeError(
f"path should be a str, not {type(self._raw_path).__name__!r}")
self._resolving = False
def with_segments(self, *pathsegments):
"""Construct a new path object from any number of path-like objects.
Subclasses may override this method to customize how new path objects
are created from methods like `iterdir()`.
"""
return type(self)(*pathsegments)
def __str__(self):
"""Return the string representation of the path, suitable for
passing to system calls."""
return self._raw_path
def as_posix(self):
"""Return the string representation of the path with forward (/)
slashes."""
return str(self).replace(self.parser.sep, '/')
@property
def drive(self):
"""The drive prefix (letter or UNC path), if any."""
return self.parser.splitdrive(self.anchor)[0]
@property
def root(self):
"""The root of the path, if any."""
return self.parser.splitdrive(self.anchor)[1]
@property
def anchor(self):
"""The concatenation of the drive and root, or ''."""
return self._stack[0]
@property
def name(self):
"""The final path component, if any."""
return self.parser.split(self._raw_path)[1]
@property
def suffix(self):
"""
The final component's last suffix, if any.
This includes the leading period. For example: '.txt'
"""
name = self.name
i = name.rfind('.')
if 0 < i < len(name) - 1:
return name[i:]
else:
return ''
@property
def suffixes(self):
"""
A list of the final component's suffixes, if any.
These include the leading periods. For example: ['.tar', '.gz']
"""
name = self.name
if name.endswith('.'):
return []
name = name.lstrip('.')
return ['.' + suffix for suffix in name.split('.')[1:]]
@property
def stem(self):
"""The final path component, minus its last suffix."""
name = self.name
i = name.rfind('.')
if 0 < i < len(name) - 1:
return name[:i]
else:
return name
def with_name(self, name):
"""Return a new path with the file name changed."""
split = self.parser.split
if split(name)[0]:
raise ValueError(f"Invalid name {name!r}")
return self.with_segments(split(self._raw_path)[0], name)
def with_stem(self, stem):
"""Return a new path with the stem changed."""
suffix = self.suffix
if not suffix:
return self.with_name(stem)
elif not stem:
# If the suffix is non-empty, we can't make the stem empty.
raise ValueError(f"{self!r} has a non-empty suffix")
else:
return self.with_name(stem + suffix)
def with_suffix(self, suffix):
"""Return a new path with the file suffix changed. If the path
has no suffix, add given suffix. If the given suffix is an empty
string, remove the suffix from the path.
"""
stem = self.stem
if not stem:
# If the stem is empty, we can't make the suffix non-empty.
raise ValueError(f"{self!r} has an empty name")
elif suffix and not (suffix.startswith('.') and len(suffix) > 1):
raise ValueError(f"Invalid suffix {suffix!r}")
else:
return self.with_name(stem + suffix)
def relative_to(self, other, *, 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
related to the other path), raise ValueError.
The *walk_up* parameter controls whether `..` may be used to resolve
the path.
"""
if not isinstance(other, PurePathBase):
other = self.with_segments(other)
anchor0, parts0 = self._stack
anchor1, parts1 = other._stack
if anchor0 != anchor1:
raise ValueError(f"{self._raw_path!r} and {other._raw_path!r} have different anchors")
while parts0 and parts1 and parts0[-1] == parts1[-1]:
parts0.pop()
parts1.pop()
for part in parts1:
if not part or part == '.':
pass
elif not walk_up:
raise ValueError(f"{self._raw_path!r} is not in the subpath of {other._raw_path!r}")
elif part == '..':
raise ValueError(f"'..' segment in {other._raw_path!r} cannot be walked")
else:
parts0.append('..')
return self.with_segments('', *reversed(parts0))
def is_relative_to(self, other):
"""Return True if the path is relative to another path or False.
"""
if not isinstance(other, PurePathBase):
other = self.with_segments(other)
anchor0, parts0 = self._stack
anchor1, parts1 = other._stack
if anchor0 != anchor1:
return False
while parts0 and parts1 and parts0[-1] == parts1[-1]:
parts0.pop()
parts1.pop()
for part in parts1:
if part and part != '.':
return False
return True
@property
def parts(self):
"""An object providing sequence-like access to the
components in the filesystem path."""
anchor, parts = self._stack
if anchor:
parts.append(anchor)
return tuple(reversed(parts))
def joinpath(self, *pathsegments):
"""Combine this path with one or several arguments, and return a
new path representing either a subpath (if all arguments are relative
paths) or a totally different path (if one of the arguments is
anchored).
"""
return self.with_segments(self._raw_path, *pathsegments)
def __truediv__(self, key):
try:
return self.with_segments(self._raw_path, key)
except TypeError:
return NotImplemented
def __rtruediv__(self, key):
try:
return self.with_segments(key, self._raw_path)
except TypeError:
return NotImplemented
@property
def _stack(self):
"""
Split the path into a 2-tuple (anchor, parts), where *anchor* is the
uppermost parent of the path (equivalent to path.parents[-1]), and
*parts* is a reversed list of parts following the anchor.
"""
split = self.parser.split
path = self._raw_path
parent, name = split(path)
names = []
while path != parent:
names.append(name)
path = parent
parent, name = split(path)
return path, names
@property
def parent(self):
"""The logical parent of the path."""
path = self._raw_path
parent = self.parser.split(path)[0]
if path != parent:
parent = self.with_segments(parent)
parent._resolving = self._resolving
return parent
return self
@property
def parents(self):
"""A sequence of this path's logical parents."""
split = self.parser.split
path = self._raw_path
parent = split(path)[0]
parents = []
while path != parent:
parents.append(self.with_segments(parent))
path = parent
parent = split(path)[0]
return tuple(parents)
def is_absolute(self):
"""True if the path is absolute (has both a root and, if applicable,
a drive)."""
return self.parser.isabs(self._raw_path)
@property
def _pattern_str(self):
"""The path expressed as a string, for use in pattern-matching."""
return str(self)
def match(self, path_pattern, *, case_sensitive=None):
"""
Return True if this path matches the given pattern. If the pattern is
relative, matching is done from the right; otherwise, the entire path
is matched. The recursive wildcard '**' is *not* supported by this
method.
"""
if not isinstance(path_pattern, PurePathBase):
path_pattern = self.with_segments(path_pattern)
if case_sensitive is None:
case_sensitive = _is_case_sensitive(self.parser)
sep = path_pattern.parser.sep
path_parts = self.parts[::-1]
pattern_parts = path_pattern.parts[::-1]
if not pattern_parts:
raise ValueError("empty pattern")
if len(path_parts) < len(pattern_parts):
return False
if len(path_parts) > len(pattern_parts) and path_pattern.anchor:
return False
globber = self._globber(sep, case_sensitive)
for path_part, pattern_part in zip(path_parts, pattern_parts):
match = globber.compile(pattern_part)
if match(path_part) is None:
return False
return True
def full_match(self, pattern, *, case_sensitive=None):
"""
Return True if this path matches the given glob-style pattern. The
pattern is matched against the entire path.
"""
if not isinstance(pattern, PurePathBase):
pattern = self.with_segments(pattern)
if case_sensitive is None:
case_sensitive = _is_case_sensitive(self.parser)
globber = self._globber(pattern.parser.sep, case_sensitive, recursive=True)
match = globber.compile(pattern._pattern_str)
return match(self._pattern_str) is not None
class PathBase(PurePathBase):
"""Base class for concrete path objects.
This class provides dummy implementations for many methods that derived
classes can override selectively; the default implementations raise
UnsupportedOperation. The most basic methods, such as stat() and open(),
directly raise UnsupportedOperation; these basic methods are called by
other methods such as is_dir() and read_text().
The Path class derives this class to implement local filesystem paths.
Users may derive their own classes to implement virtual filesystem paths,
such as paths in archive files or on remote storage systems.
"""
__slots__ = ()
# Maximum number of symlinks to follow in resolve()
_max_symlinks = 40
@classmethod
def _unsupported_msg(cls, attribute):
return f"{cls.__name__}.{attribute} is unsupported"
def stat(self, *, follow_symlinks=True):
"""
Return the result of the stat() system call on this path, like
os.stat() does.
"""
raise UnsupportedOperation(self._unsupported_msg('stat()'))
def lstat(self):
"""
Like stat(), except if the path points to a symlink, the symlink's
status information is returned, rather than its target's.
"""
return self.stat(follow_symlinks=False)
# Convenience functions for querying the stat results
def exists(self, *, follow_symlinks=True):
"""
Whether this path exists.
This method normally follows symlinks; to check whether a symlink exists,
add the argument follow_symlinks=False.
"""
try:
self.stat(follow_symlinks=follow_symlinks)
except OSError as e:
if not _ignore_error(e):
raise
return False
except ValueError:
# Non-encodable path
return False
return True
def is_dir(self, *, follow_symlinks=True):
"""
Whether this path is a directory.
"""
try:
return S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode)
except OSError as e:
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False
def is_file(self, *, follow_symlinks=True):
"""
Whether this path is a regular file (also True for symlinks pointing
to regular files).
"""
try:
return S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode)
except OSError as e:
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False
def is_mount(self):
"""
Check if this path is a mount point
"""
# Need to exist and be a dir
if not self.exists() or not self.is_dir():
return False
try:
parent_dev = self.parent.stat().st_dev
except OSError:
return False
dev = self.stat().st_dev
if dev != parent_dev:
return True
ino = self.stat().st_ino
parent_ino = self.parent.stat().st_ino
return ino == parent_ino
def is_symlink(self):
"""
Whether this path is a symbolic link.
"""
try:
return S_ISLNK(self.lstat().st_mode)
except OSError as e:
if not _ignore_error(e):
raise
# Path doesn't exist
return False
except ValueError:
# Non-encodable path
return False
def is_junction(self):
"""
Whether this path is a junction.
"""
# Junctions are a Windows-only feature, not present in POSIX nor the
# majority of virtual filesystems. There is no cross-platform idiom
# to check for junctions (using stat().st_mode).
return False
def is_block_device(self):
"""
Whether this path is a block device.
"""
try:
return S_ISBLK(self.stat().st_mode)
except OSError as e:
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False
def is_char_device(self):
"""
Whether this path is a character device.
"""
try:
return S_ISCHR(self.stat().st_mode)
except OSError as e:
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False
def is_fifo(self):
"""
Whether this path is a FIFO.
"""
try:
return S_ISFIFO(self.stat().st_mode)
except OSError as e:
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False
def is_socket(self):
"""
Whether this path is a socket.
"""
try:
return S_ISSOCK(self.stat().st_mode)
except OSError as e:
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False
def samefile(self, other_path):
"""Return whether other_path is the same or not as this file
(as returned by os.path.samefile()).
"""
st = self.stat()
try:
other_st = other_path.stat()
except AttributeError:
other_st = self.with_segments(other_path).stat()
return (st.st_ino == other_st.st_ino and
st.st_dev == other_st.st_dev)
def open(self, mode='r', buffering=-1, encoding=None,
errors=None, newline=None):
"""
Open the file pointed to by this path and return a file object, as
the built-in open() function does.
"""
raise UnsupportedOperation(self._unsupported_msg('open()'))
def read_bytes(self):
"""
Open the file in bytes mode, read it, and close the file.
"""
with self.open(mode='rb') as f:
return f.read()
def read_text(self, encoding=None, errors=None, newline=None):
"""
Open the file in text mode, read it, and close the file.
"""
with self.open(mode='r', encoding=encoding, errors=errors, newline=newline) as f:
return f.read()
def write_bytes(self, data):
"""
Open the file in bytes mode, write to it, and close the file.
"""
# type-check for the buffer interface before truncating the file
view = memoryview(data)
with self.open(mode='wb') as f:
return f.write(view)
def write_text(self, data, encoding=None, errors=None, newline=None):
"""
Open the file in text mode, write to it, and close the file.
"""
if not isinstance(data, str):
raise TypeError('data must be str, not %s' %
data.__class__.__name__)
with self.open(mode='w', encoding=encoding, errors=errors, newline=newline) as f:
return f.write(data)
def iterdir(self):
"""Yield path objects of the directory contents.
The children are yielded in arbitrary order, and the
special entries '.' and '..' are not included.
"""
raise UnsupportedOperation(self._unsupported_msg('iterdir()'))
def _glob_selector(self, parts, case_sensitive, recurse_symlinks):
if case_sensitive is None:
case_sensitive = _is_case_sensitive(self.parser)
case_pedantic = False
else:
# The user has expressed a case sensitivity choice, but we don't
# know the case sensitivity of the underlying filesystem, so we
# must use scandir() for everything, including non-wildcard parts.
case_pedantic = True
recursive = True if recurse_symlinks else _no_recurse_symlinks
globber = self._globber(self.parser.sep, case_sensitive, case_pedantic, recursive)
return globber.selector(parts)
def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):
"""Iterate over this subtree and yield all existing files (of any
kind, including directories) matching the given relative pattern.
"""
if not isinstance(pattern, PurePathBase):
pattern = self.with_segments(pattern)
anchor, parts = pattern._stack
if anchor:
raise NotImplementedError("Non-relative patterns are unsupported")
select = self._glob_selector(parts, case_sensitive, recurse_symlinks)
return select(self)
def rglob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):
"""Recursively yield all existing files (of any kind, including
directories) matching the given relative pattern, anywhere in
this subtree.
"""
if not isinstance(pattern, PurePathBase):
pattern = self.with_segments(pattern)
pattern = '**' / pattern
return self.glob(pattern, case_sensitive=case_sensitive, recurse_symlinks=recurse_symlinks)
def walk(self, top_down=True, on_error=None, follow_symlinks=False):
"""Walk the directory tree from this directory, similar to os.walk()."""
paths = [self]
while paths:
path = paths.pop()
if isinstance(path, tuple):
yield path
continue
dirnames = []
filenames = []
if not top_down:
paths.append((path, dirnames, filenames))
try:
for child in path.iterdir():
try:
if child.is_dir(follow_symlinks=follow_symlinks):
if not top_down:
paths.append(child)
dirnames.append(child.name)
else:
filenames.append(child.name)
except OSError:
filenames.append(child.name)
except OSError as error:
if on_error is not None:
on_error(error)
if not top_down:
while not isinstance(paths.pop(), tuple):
pass
continue
if top_down:
yield path, dirnames, filenames
paths += [path.joinpath(d) for d in reversed(dirnames)]
def absolute(self):
"""Return an absolute version of this path
No normalization or symlink resolution is performed.
Use resolve() to resolve symlinks and remove '..' segments.
"""
raise UnsupportedOperation(self._unsupported_msg('absolute()'))
@classmethod
def cwd(cls):
"""Return a new path pointing to the current working directory."""
# We call 'absolute()' rather than using 'os.getcwd()' directly to
# enable users to replace the implementation of 'absolute()' in a
# subclass and benefit from the new behaviour here. This works because
# os.path.abspath('.') == os.getcwd().
return cls('').absolute()
def expanduser(self):
""" Return a new path with expanded ~ and ~user constructs
(as returned by os.path.expanduser)
"""
raise UnsupportedOperation(self._unsupported_msg('expanduser()'))
@classmethod
def home(cls):
"""Return a new path pointing to expanduser('~').
"""
return cls("~").expanduser()
def readlink(self):
"""
Return the path to which the symbolic link points.
"""
raise UnsupportedOperation(self._unsupported_msg('readlink()'))
readlink._supported = False
def resolve(self, strict=False):
"""
Make the path absolute, resolving all symlinks on the way and also
normalizing it.
"""
if self._resolving:
return self
path_root, parts = self._stack
path = self.with_segments(path_root)
try:
path = path.absolute()
except UnsupportedOperation:
path_tail = []
else:
path_root, path_tail = path._stack
path_tail.reverse()
# If the user has *not* overridden the `readlink()` method, then symlinks are unsupported
# and (in non-strict mode) we can improve performance by not calling `stat()`.
querying = strict or getattr(self.readlink, '_supported', True)
link_count = 0
while parts:
part = parts.pop()
if not part or part == '.':
continue
if part == '..':
if not path_tail:
if path_root:
# Delete '..' segment immediately following root
continue
elif path_tail[-1] != '..':
# Delete '..' segment and its predecessor
path_tail.pop()
continue
path_tail.append(part)
if querying and part != '..':
path = self.with_segments(path_root + self.parser.sep.join(path_tail))
path._resolving = True
try:
st = path.stat(follow_symlinks=False)
if S_ISLNK(st.st_mode):
# Like Linux and macOS, raise OSError(errno.ELOOP) if too many symlinks are
# encountered during resolution.
link_count += 1
if link_count >= self._max_symlinks:
raise OSError(ELOOP, "Too many symbolic links in path", self._raw_path)
target_root, target_parts = path.readlink()._stack
# If the symlink target is absolute (like '/etc/hosts'), set the current
# path to its uppermost parent (like '/').
if target_root:
path_root = target_root
path_tail.clear()
else:
path_tail.pop()
# Add the symlink target's reversed tail parts (like ['hosts', 'etc']) to
# the stack of unresolved path parts.
parts.extend(target_parts)
continue
elif parts and not S_ISDIR(st.st_mode):
raise NotADirectoryError(ENOTDIR, "Not a directory", self._raw_path)
except OSError:
if strict:
raise
else:
querying = False
return self.with_segments(path_root + self.parser.sep.join(path_tail))
def symlink_to(self, target, target_is_directory=False):
"""
Make this path a symlink pointing to the target path.
Note the order of arguments (link, target) is the reverse of os.symlink.
"""
raise UnsupportedOperation(self._unsupported_msg('symlink_to()'))
def hardlink_to(self, target):
"""
Make this path a hard link pointing to the same file as *target*.
Note the order of arguments (self, target) is the reverse of os.link's.
"""
raise UnsupportedOperation(self._unsupported_msg('hardlink_to()'))
def touch(self, mode=0o666, exist_ok=True):
"""
Create this file with the given access mode, if it doesn't exist.
"""
raise UnsupportedOperation(self._unsupported_msg('touch()'))
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
"""
Create a new directory at this given path.
"""
raise UnsupportedOperation(self._unsupported_msg('mkdir()'))
def rename(self, target):
"""
Rename this path to the target path.
The target path may be absolute or relative. Relative paths are
interpreted relative to the current working directory, *not* the
directory of the Path object.
Returns the new Path instance pointing to the target path.
"""
raise UnsupportedOperation(self._unsupported_msg('rename()'))
def replace(self, target):
"""
Rename this path to the target path, overwriting if that path exists.
The target path may be absolute or relative. Relative paths are
interpreted relative to the current working directory, *not* the
directory of the Path object.
Returns the new Path instance pointing to the target path.
"""
raise UnsupportedOperation(self._unsupported_msg('replace()'))
def chmod(self, mode, *, follow_symlinks=True):
"""
Change the permissions of the path, like os.chmod().
"""
raise UnsupportedOperation(self._unsupported_msg('chmod()'))
def lchmod(self, mode):
"""
Like chmod(), except if the path points to a symlink, the symlink's
permissions are changed, rather than its target's.
"""
self.chmod(mode, follow_symlinks=False)
def unlink(self, missing_ok=False):
"""
Remove this file or link.
If the path is a directory, use rmdir() instead.
"""
raise UnsupportedOperation(self._unsupported_msg('unlink()'))
def rmdir(self):
"""
Remove this directory. The directory must be empty.
"""
raise UnsupportedOperation(self._unsupported_msg('rmdir()'))
def owner(self, *, follow_symlinks=True):
"""
Return the login name of the file owner.
"""
raise UnsupportedOperation(self._unsupported_msg('owner()'))
def group(self, *, follow_symlinks=True):
"""
Return the group name of the file gid.
"""
raise UnsupportedOperation(self._unsupported_msg('group()'))
@classmethod
def from_uri(cls, uri):
"""Return a new path from the given 'file' URI."""
raise UnsupportedOperation(cls._unsupported_msg('from_uri()'))
def as_uri(self):
"""Return the path as a URI."""
raise UnsupportedOperation(self._unsupported_msg('as_uri()'))
+7 -856
View File
@@ -1,861 +1,12 @@
import io
import ntpath
import operator
import os
import posixpath
import sys
import warnings
from glob import _StringGlobber
from itertools import chain
from _collections_abc import Sequence
try:
import pwd
except ImportError:
pwd = None
try:
import grp
except ImportError:
grp = None
from ._abc import UnsupportedOperation, PurePathBase, PathBase
"""
This module exists so that pathlib objects pickled under Python 3.13 can be
unpickled in 3.14+.
"""
from pathlib import *
__all__ = [
"UnsupportedOperation",
"PurePath", "PurePosixPath", "PureWindowsPath",
"Path", "PosixPath", "WindowsPath",
]
class _PathParents(Sequence):
"""This object provides sequence-like access to the logical ancestors
of a path. Don't try to construct it yourself."""
__slots__ = ('_path', '_drv', '_root', '_tail')
def __init__(self, path):
self._path = path
self._drv = path.drive
self._root = path.root
self._tail = path._tail
def __len__(self):
return len(self._tail)
def __getitem__(self, idx):
if isinstance(idx, slice):
return tuple(self[i] for i in range(*idx.indices(len(self))))
if idx >= len(self) or idx < -len(self):
raise IndexError(idx)
if idx < 0:
idx += len(self)
return self._path._from_parsed_parts(self._drv, self._root,
self._tail[:-idx - 1])
def __repr__(self):
return "<{}.parents>".format(type(self._path).__name__)
class PurePath(PurePathBase):
"""Base class for manipulating paths without I/O.
PurePath represents a filesystem path and offers operations which
don't imply any actual filesystem I/O. Depending on your system,
instantiating a PurePath will return either a PurePosixPath or a
PureWindowsPath object. You can also instantiate either of these classes
directly, regardless of your system.
"""
__slots__ = (
# The `_raw_paths` slot stores unnormalized string paths. This is set
# in the `__init__()` method.
'_raw_paths',
# The `_drv`, `_root` and `_tail_cached` slots store parsed and
# normalized parts of the path. They are set when any of the `drive`,
# `root` or `_tail` properties are accessed for the first time. The
# three-part division corresponds to the result of
# `os.path.splitroot()`, except that the tail is further split on path
# separators (i.e. it is a list of strings), and that the root and
# tail are normalized.
'_drv', '_root', '_tail_cached',
# 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`
'_str',
# The `_str_normcase_cached` slot stores the string path with
# normalized case. It is set when the `_str_normcase` property is
# accessed for the first time. It's used to implement `__eq__()`
# `__hash__()`, and `_parts_normcase`
'_str_normcase_cached',
# The `_parts_normcase_cached` slot stores the case-normalized
# string path after splitting on path separators. It's set when the
# `_parts_normcase` property is accessed for the first time. It's used
# to implement comparison methods like `__lt__()`.
'_parts_normcase_cached',
# The `_hash` slot stores the hash of the case-normalized string
# path. It's set when `__hash__()` is called for the first time.
'_hash',
)
parser = os.path
_globber = _StringGlobber
def __new__(cls, *args, **kwargs):
"""Construct a PurePath from one or several strings and or existing
PurePath objects. The strings and path objects are combined so as
to yield a canonicalized path, which is incorporated into the
new PurePath object.
"""
if cls is PurePath:
cls = PureWindowsPath if os.name == 'nt' else PurePosixPath
return object.__new__(cls)
def __init__(self, *args):
paths = []
for arg in args:
if isinstance(arg, PurePath):
if arg.parser is not self.parser:
# GH-103631: Convert separators for backwards compatibility.
paths.append(arg.as_posix())
else:
paths.extend(arg._raw_paths)
else:
try:
path = os.fspath(arg)
except TypeError:
path = arg
if not isinstance(path, str):
raise TypeError(
"argument should be a str or an os.PathLike "
"object where __fspath__ returns a str, "
f"not {type(path).__name__!r}")
paths.append(path)
# Avoid calling super().__init__, as an optimisation
self._raw_paths = paths
def joinpath(self, *pathsegments):
"""Combine this path with one or several arguments, and return a
new path representing either a subpath (if all arguments are relative
paths) or a totally different path (if one of the arguments is
anchored).
"""
return self.with_segments(self, *pathsegments)
def __truediv__(self, key):
try:
return self.with_segments(self, key)
except TypeError:
return NotImplemented
def __rtruediv__(self, key):
try:
return self.with_segments(key, self)
except TypeError:
return NotImplemented
def __reduce__(self):
return self.__class__, tuple(self._raw_paths)
def __repr__(self):
return "{}({!r})".format(self.__class__.__name__, self.as_posix())
def __fspath__(self):
return str(self)
def __bytes__(self):
"""Return the bytes representation of the path. This is only
recommended to use under Unix."""
return os.fsencode(self)
@property
def _str_normcase(self):
# String with normalized case, for hashing and equality checks
try:
return self._str_normcase_cached
except AttributeError:
if self.parser is posixpath:
self._str_normcase_cached = str(self)
else:
self._str_normcase_cached = str(self).lower()
return self._str_normcase_cached
def __hash__(self):
try:
return self._hash
except AttributeError:
self._hash = hash(self._str_normcase)
return self._hash
def __eq__(self, other):
if not isinstance(other, PurePath):
return NotImplemented
return self._str_normcase == other._str_normcase and self.parser is other.parser
@property
def _parts_normcase(self):
# Cached parts with normalized case, for comparisons.
try:
return self._parts_normcase_cached
except AttributeError:
self._parts_normcase_cached = self._str_normcase.split(self.parser.sep)
return self._parts_normcase_cached
def __lt__(self, other):
if not isinstance(other, PurePath) or self.parser is not other.parser:
return NotImplemented
return self._parts_normcase < other._parts_normcase
def __le__(self, other):
if not isinstance(other, PurePath) or self.parser is not other.parser:
return NotImplemented
return self._parts_normcase <= other._parts_normcase
def __gt__(self, other):
if not isinstance(other, PurePath) or self.parser is not other.parser:
return NotImplemented
return self._parts_normcase > other._parts_normcase
def __ge__(self, other):
if not isinstance(other, PurePath) or self.parser is not other.parser:
return NotImplemented
return self._parts_normcase >= other._parts_normcase
def __str__(self):
"""Return the string representation of the path, suitable for
passing to system calls."""
try:
return self._str
except AttributeError:
self._str = self._format_parsed_parts(self.drive, self.root,
self._tail) or '.'
return self._str
@classmethod
def _format_parsed_parts(cls, drv, root, tail):
if drv or root:
return drv + root + cls.parser.sep.join(tail)
elif tail and cls.parser.splitdrive(tail[0])[0]:
tail = ['.'] + tail
return cls.parser.sep.join(tail)
def _from_parsed_parts(self, drv, root, tail):
path = self._from_parsed_string(self._format_parsed_parts(drv, root, tail))
path._drv = drv
path._root = root
path._tail_cached = tail
return path
def _from_parsed_string(self, path_str):
path = self.with_segments(path_str)
path._str = path_str or '.'
return path
@classmethod
def _parse_path(cls, path):
if not path:
return '', '', []
sep = cls.parser.sep
altsep = cls.parser.altsep
if altsep:
path = path.replace(altsep, sep)
drv, root, rel = cls.parser.splitroot(path)
if not root and drv.startswith(sep) and not drv.endswith(sep):
drv_parts = drv.split(sep)
if len(drv_parts) == 4 and drv_parts[2] not in '?.':
# e.g. //server/share
root = sep
elif len(drv_parts) == 6:
# 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
@property
def _raw_path(self):
"""The joined but unnormalized path."""
paths = self._raw_paths
if len(paths) == 0:
path = ''
elif len(paths) == 1:
path = paths[0]
else:
path = self.parser.join(*paths)
return path
@property
def drive(self):
"""The drive prefix (letter or UNC path), if any."""
try:
return self._drv
except AttributeError:
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
return self._drv
@property
def root(self):
"""The root of the path, if any."""
try:
return self._root
except AttributeError:
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
return self._root
@property
def _tail(self):
try:
return self._tail_cached
except AttributeError:
self._drv, self._root, self._tail_cached = self._parse_path(self._raw_path)
return self._tail_cached
@property
def anchor(self):
"""The concatenation of the drive and root, or ''."""
return self.drive + self.root
@property
def parts(self):
"""An object providing sequence-like access to the
components in the filesystem path."""
if self.drive or self.root:
return (self.drive + self.root,) + tuple(self._tail)
else:
return tuple(self._tail)
@property
def parent(self):
"""The logical parent of the path."""
drv = self.drive
root = self.root
tail = self._tail
if not tail:
return self
return self._from_parsed_parts(drv, root, tail[:-1])
@property
def parents(self):
"""A sequence of this path's logical parents."""
# The value of this property should not be cached on the path object,
# as doing so would introduce a reference cycle.
return _PathParents(self)
@property
def name(self):
"""The final path component, if any."""
tail = self._tail
if not tail:
return ''
return tail[-1]
def with_name(self, name):
"""Return a new path with the file name changed."""
p = self.parser
if not name or p.sep in name or (p.altsep and p.altsep in name) or name == '.':
raise ValueError(f"Invalid name {name!r}")
tail = self._tail.copy()
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)
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
related to the other path), raise ValueError.
The *walk_up* parameter controls whether `..` may be used to resolve
the path.
"""
if _deprecated:
msg = ("support for supplying more than one positional argument "
"to pathlib.PurePath.relative_to() is deprecated and "
"scheduled for removal in Python 3.14")
warnings.warn(msg, DeprecationWarning, stacklevel=2)
other = self.with_segments(other, *_deprecated)
elif not isinstance(other, PurePath):
other = self.with_segments(other)
for step, path in enumerate(chain([other], other.parents)):
if path == self or path in self.parents:
break
elif not walk_up:
raise ValueError(f"{str(self)!r} is not in the subpath of {str(other)!r}")
elif path.name == '..':
raise ValueError(f"'..' segment in {str(other)!r} cannot be walked")
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)
def is_relative_to(self, other, /, *_deprecated):
"""Return True if the path is relative to another path or False.
"""
if _deprecated:
msg = ("support for supplying more than one argument to "
"pathlib.PurePath.is_relative_to() is deprecated and "
"scheduled for removal in Python 3.14")
warnings.warn(msg, DeprecationWarning, stacklevel=2)
other = self.with_segments(other, *_deprecated)
elif not isinstance(other, PurePath):
other = self.with_segments(other)
return other == self or other in self.parents
def is_absolute(self):
"""True if the path is absolute (has both a root and, if applicable,
a drive)."""
if self.parser is posixpath:
# Optimization: work with raw paths on POSIX.
for path in self._raw_paths:
if path.startswith('/'):
return True
return False
return self.parser.isabs(self)
def is_reserved(self):
"""Return True if the path contains one of the special names reserved
by the system, if any."""
msg = ("pathlib.PurePath.is_reserved() is deprecated and scheduled "
"for removal in Python 3.15. Use os.path.isreserved() to "
"detect reserved paths on Windows.")
warnings.warn(msg, DeprecationWarning, stacklevel=2)
if self.parser is ntpath:
return self.parser.isreserved(self)
return False
def as_uri(self):
"""Return the path as a URI."""
if not self.is_absolute():
raise ValueError("relative path can't be expressed as a file URI")
drive = self.drive
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)
from urllib.parse import quote_from_bytes
return prefix + quote_from_bytes(os.fsencode(path))
@property
def _pattern_str(self):
"""The path expressed as a string, for use in pattern-matching."""
# The string representation of an empty path is a single dot ('.'). Empty
# paths shouldn't match wildcards, so we change it to the empty string.
path_str = str(self)
return '' if path_str == '.' else path_str
# Subclassing os.PathLike makes isinstance() checks slower,
# which in turn makes Path construction slower. Register instead!
os.PathLike.register(PurePath)
class PurePosixPath(PurePath):
"""PurePath subclass for non-Windows systems.
On a POSIX system, instantiating a PurePath should return this object.
However, you can also instantiate it directly on any system.
"""
parser = posixpath
__slots__ = ()
class PureWindowsPath(PurePath):
"""PurePath subclass for Windows systems.
On a Windows system, instantiating a PurePath should return this object.
However, you can also instantiate it directly on any system.
"""
parser = ntpath
__slots__ = ()
class Path(PathBase, PurePath):
"""PurePath subclass that can make system calls.
Path represents a filesystem path but unlike PurePath, also offers
methods to do system calls on path objects. Depending on your system,
instantiating a Path will return either a PosixPath or a WindowsPath
object. You can also instantiate a PosixPath or WindowsPath directly,
but cannot instantiate a WindowsPath on a POSIX system or vice versa.
"""
__slots__ = ()
as_uri = PurePath.as_uri
@classmethod
def _unsupported_msg(cls, attribute):
return f"{cls.__name__}.{attribute} is unsupported on this system"
def __init__(self, *args, **kwargs):
if kwargs:
msg = ("support for supplying keyword arguments to pathlib.PurePath "
"is deprecated and scheduled for removal in Python {remove}")
warnings._deprecated("pathlib.PurePath(**kwargs)", msg, remove=(3, 14))
super().__init__(*args)
def __new__(cls, *args, **kwargs):
if cls is Path:
cls = WindowsPath if os.name == 'nt' else PosixPath
return object.__new__(cls)
def stat(self, *, follow_symlinks=True):
"""
Return the result of the stat() system call on this path, like
os.stat() does.
"""
return os.stat(self, follow_symlinks=follow_symlinks)
def is_mount(self):
"""
Check if this path is a mount point
"""
return os.path.ismount(self)
def is_junction(self):
"""
Whether this path is a junction.
"""
return os.path.isjunction(self)
def open(self, mode='r', buffering=-1, encoding=None,
errors=None, newline=None):
"""
Open the file pointed to by this path and return a file object, as
the built-in open() function does.
"""
if "b" not in mode:
encoding = io.text_encoding(encoding)
return io.open(self, mode, buffering, encoding, errors, newline)
def read_text(self, encoding=None, errors=None, newline=None):
"""
Open the file in text mode, read it, and close the file.
"""
# Call io.text_encoding() here to ensure any warning is raised at an
# appropriate stack level.
encoding = io.text_encoding(encoding)
return PathBase.read_text(self, encoding, errors, newline)
def write_text(self, data, encoding=None, errors=None, newline=None):
"""
Open the file in text mode, write to it, and close the file.
"""
# Call io.text_encoding() here to ensure any warning is raised at an
# appropriate stack level.
encoding = io.text_encoding(encoding)
return PathBase.write_text(self, data, encoding, errors, newline)
_remove_leading_dot = operator.itemgetter(slice(2, None))
_remove_trailing_slash = operator.itemgetter(slice(-1))
def _filter_trailing_slash(self, paths):
sep = self.parser.sep
anchor_len = len(self.anchor)
for path_str in paths:
if len(path_str) > anchor_len and path_str[-1] == sep:
path_str = path_str[:-1]
yield path_str
def iterdir(self):
"""Yield path objects of the directory contents.
The children are yielded in arbitrary order, and the
special entries '.' and '..' are not included.
"""
root_dir = str(self)
with os.scandir(root_dir) as scandir_it:
paths = [entry.path for entry in scandir_it]
if root_dir == '.':
paths = map(self._remove_leading_dot, paths)
return map(self._from_parsed_string, paths)
def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=False):
"""Iterate over this subtree and yield all existing files (of any
kind, including directories) matching the given relative pattern.
"""
sys.audit("pathlib.Path.glob", self, pattern)
if not isinstance(pattern, PurePath):
pattern = self.with_segments(pattern)
if pattern.anchor:
raise NotImplementedError("Non-relative patterns are unsupported")
parts = pattern._tail.copy()
if not parts:
raise ValueError("Unacceptable pattern: {!r}".format(pattern))
raw = pattern._raw_path
if raw[-1] in (self.parser.sep, self.parser.altsep):
# GH-65238: pathlib doesn't preserve trailing slash. Add it back.
parts.append('')
select = self._glob_selector(parts[::-1], case_sensitive, recurse_symlinks)
root = str(self)
paths = select(root)
# Normalize results
if root == '.':
paths = map(self._remove_leading_dot, paths)
if parts[-1] == '':
paths = map(self._remove_trailing_slash, paths)
elif parts[-1] == '**':
paths = self._filter_trailing_slash(paths)
paths = map(self._from_parsed_string, paths)
return paths
def rglob(self, pattern, *, case_sensitive=None, recurse_symlinks=False):
"""Recursively yield all existing files (of any kind, including
directories) matching the given relative pattern, anywhere in
this subtree.
"""
sys.audit("pathlib.Path.rglob", self, pattern)
if not isinstance(pattern, PurePath):
pattern = self.with_segments(pattern)
pattern = '**' / pattern
return self.glob(pattern, case_sensitive=case_sensitive, recurse_symlinks=recurse_symlinks)
def walk(self, top_down=True, on_error=None, follow_symlinks=False):
"""Walk the directory tree from this directory, similar to os.walk()."""
sys.audit("pathlib.Path.walk", self, on_error, follow_symlinks)
root_dir = str(self)
if not follow_symlinks:
follow_symlinks = os._walk_symlinks_as_files
results = os.walk(root_dir, top_down, on_error, follow_symlinks)
for path_str, dirnames, filenames in results:
if root_dir == '.':
path_str = path_str[2:]
yield self._from_parsed_string(path_str), dirnames, filenames
def absolute(self):
"""Return an absolute version of this path
No normalization or symlink resolution is performed.
Use resolve() to resolve symlinks and remove '..' segments.
"""
if self.is_absolute():
return self
if self.root:
drive = os.path.splitroot(os.getcwd())[0]
return self._from_parsed_parts(drive, self.root, self._tail)
if self.drive:
# There is a CWD on each drive-letter drive.
cwd = os.path.abspath(self.drive)
else:
cwd = os.getcwd()
if not self._tail:
# Fast path for "empty" paths, e.g. Path("."), Path("") or Path().
# We pass only one argument to with_segments() to avoid the cost
# of joining, and we exploit the fact that getcwd() returns a
# fully-normalized string by storing it in _str. This is used to
# implement Path.cwd().
return self._from_parsed_string(cwd)
drive, root, rel = os.path.splitroot(cwd)
if not rel:
return self._from_parsed_parts(drive, root, self._tail)
tail = rel.split(self.parser.sep)
tail.extend(self._tail)
return self._from_parsed_parts(drive, root, tail)
def resolve(self, strict=False):
"""
Make the path absolute, resolving all symlinks on the way and also
normalizing it.
"""
return self.with_segments(os.path.realpath(self, strict=strict))
if pwd:
def owner(self, *, follow_symlinks=True):
"""
Return the login name of the file owner.
"""
uid = self.stat(follow_symlinks=follow_symlinks).st_uid
return pwd.getpwuid(uid).pw_name
if grp:
def group(self, *, follow_symlinks=True):
"""
Return the group name of the file gid.
"""
gid = self.stat(follow_symlinks=follow_symlinks).st_gid
return grp.getgrgid(gid).gr_name
if hasattr(os, "readlink"):
def readlink(self):
"""
Return the path to which the symbolic link points.
"""
return self.with_segments(os.readlink(self))
def touch(self, mode=0o666, exist_ok=True):
"""
Create this file with the given access mode, if it doesn't exist.
"""
if exist_ok:
# First try to bump modification time
# Implementation note: GNU touch uses the UTIME_NOW option of
# the utimensat() / futimens() functions.
try:
os.utime(self, None)
except OSError:
# Avoid exception chaining
pass
else:
return
flags = os.O_CREAT | os.O_WRONLY
if not exist_ok:
flags |= os.O_EXCL
fd = os.open(self, flags, mode)
os.close(fd)
def mkdir(self, mode=0o777, parents=False, exist_ok=False):
"""
Create a new directory at this given path.
"""
try:
os.mkdir(self, mode)
except FileNotFoundError:
if not parents or self.parent == self:
raise
self.parent.mkdir(parents=True, exist_ok=True)
self.mkdir(mode, parents=False, exist_ok=exist_ok)
except OSError:
# Cannot rely on checking for EEXIST, since the operating system
# could give priority to other errors like EACCES or EROFS
if not exist_ok or not self.is_dir():
raise
def chmod(self, mode, *, follow_symlinks=True):
"""
Change the permissions of the path, like os.chmod().
"""
os.chmod(self, mode, follow_symlinks=follow_symlinks)
def unlink(self, missing_ok=False):
"""
Remove this file or link.
If the path is a directory, use rmdir() instead.
"""
try:
os.unlink(self)
except FileNotFoundError:
if not missing_ok:
raise
def rmdir(self):
"""
Remove this directory. The directory must be empty.
"""
os.rmdir(self)
def rename(self, target):
"""
Rename this path to the target path.
The target path may be absolute or relative. Relative paths are
interpreted relative to the current working directory, *not* the
directory of the Path object.
Returns the new Path instance pointing to the target path.
"""
os.rename(self, target)
return self.with_segments(target)
def replace(self, target):
"""
Rename this path to the target path, overwriting if that path exists.
The target path may be absolute or relative. Relative paths are
interpreted relative to the current working directory, *not* the
directory of the Path object.
Returns the new Path instance pointing to the target path.
"""
os.replace(self, target)
return self.with_segments(target)
if hasattr(os, "symlink"):
def symlink_to(self, target, target_is_directory=False):
"""
Make this path a symlink pointing to the target path.
Note the order of arguments (link, target) is the reverse of os.symlink.
"""
os.symlink(target, self, target_is_directory)
if hasattr(os, "link"):
def hardlink_to(self, target):
"""
Make this path a hard link pointing to the same file as *target*.
Note the order of arguments (self, target) is the reverse of os.link's.
"""
os.link(target, self)
def expanduser(self):
""" Return a new path with expanded ~ and ~user constructs
(as returned by os.path.expanduser)
"""
if (not (self.drive or self.root) and
self._tail and self._tail[0][:1] == '~'):
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:])
return self
@classmethod
def from_uri(cls, uri):
"""Return a new path from the given 'file' URI."""
if not uri.startswith('file:'):
raise ValueError(f"URI does not start with 'file:': {uri!r}")
path = uri[5:]
if path[:3] == '///':
# Remove empty authority
path = path[2:]
elif path[:12] == '//localhost/':
# Remove 'localhost' authority
path = path[11:]
if path[:3] == '///' or (path[:1] == '/' and path[2:3] in ':|'):
# Remove slash before DOS device/UNC path
path = path[1:]
if path[1:2] == '|':
# Replace bar with colon in DOS drive
path = path[:1] + ':' + path[2:]
from urllib.parse import unquote_to_bytes
path = cls(os.fsdecode(unquote_to_bytes(path)))
if not path.is_absolute():
raise ValueError(f"URI is not absolute: {uri!r}")
return path
class PosixPath(Path, PurePosixPath):
"""Path subclass for non-Windows systems.
On a POSIX system, instantiating a Path should return this object.
"""
__slots__ = ()
if os.name == 'nt':
def __new__(cls, *args, **kwargs):
raise UnsupportedOperation(
f"cannot instantiate {cls.__name__!r} on your system")
class WindowsPath(Path, PureWindowsPath):
"""Path subclass for Windows systems.
On a Windows system, instantiating a Path should return this object.
"""
__slots__ = ()
if os.name != 'nt':
def __new__(cls, *args, **kwargs):
raise UnsupportedOperation(
f"cannot instantiate {cls.__name__!r} on your system")
]
+530
View File
@@ -0,0 +1,530 @@
"""
Low-level OS functionality wrappers used by pathlib.
"""
from errno import *
from io import TextIOWrapper, text_encoding
from stat import S_ISDIR, S_ISREG, S_ISLNK, S_IMODE
import os
import sys
try:
import fcntl
except ImportError:
fcntl = None
try:
import posix
except ImportError:
posix = None
try:
import _winapi
except ImportError:
_winapi = None
def _get_copy_blocksize(infd):
"""Determine blocksize for fastcopying on Linux.
Hopefully the whole file will be copied in a single call.
The copying itself should be performed in a loop 'till EOF is
reached (0 return) so a blocksize smaller or bigger than the actual
file size should not make any difference, also in case the file
content changes while being copied.
"""
try:
blocksize = max(os.fstat(infd).st_size, 2 ** 23) # min 8 MiB
except OSError:
blocksize = 2 ** 27 # 128 MiB
# On 32-bit architectures truncate to 1 GiB to avoid OverflowError,
# see gh-82500.
if sys.maxsize < 2 ** 32:
blocksize = min(blocksize, 2 ** 30)
return blocksize
if fcntl and hasattr(fcntl, 'FICLONE'):
def _ficlone(source_fd, target_fd):
"""
Perform a lightweight copy of two files, where the data blocks are
copied only when modified. This is known as Copy on Write (CoW),
instantaneous copy or reflink.
"""
fcntl.ioctl(target_fd, fcntl.FICLONE, source_fd)
else:
_ficlone = None
if posix and hasattr(posix, '_fcopyfile'):
def _fcopyfile(source_fd, target_fd):
"""
Copy a regular file content using high-performance fcopyfile(3)
syscall (macOS).
"""
posix._fcopyfile(source_fd, target_fd, posix._COPYFILE_DATA)
else:
_fcopyfile = None
if hasattr(os, 'copy_file_range'):
def _copy_file_range(source_fd, target_fd):
"""
Copy data from one regular mmap-like fd to another by using a
high-performance copy_file_range(2) syscall that gives filesystems
an opportunity to implement the use of reflinks or server-side
copy.
This should work on Linux >= 4.5 only.
"""
blocksize = _get_copy_blocksize(source_fd)
offset = 0
while True:
sent = os.copy_file_range(source_fd, target_fd, blocksize,
offset_dst=offset)
if sent == 0:
break # EOF
offset += sent
else:
_copy_file_range = None
if hasattr(os, 'sendfile'):
def _sendfile(source_fd, target_fd):
"""Copy data from one regular mmap-like fd to another by using
high-performance sendfile(2) syscall.
This should work on Linux >= 2.6.33 only.
"""
blocksize = _get_copy_blocksize(source_fd)
offset = 0
while True:
sent = os.sendfile(target_fd, source_fd, offset, blocksize)
if sent == 0:
break # EOF
offset += sent
else:
_sendfile = None
if _winapi and hasattr(_winapi, 'CopyFile2'):
def copyfile2(source, target):
"""
Copy from one file to another using CopyFile2 (Windows only).
"""
_winapi.CopyFile2(source, target, 0)
else:
copyfile2 = None
def copyfileobj(source_f, target_f):
"""
Copy data from file-like object source_f to file-like object target_f.
"""
try:
source_fd = source_f.fileno()
target_fd = target_f.fileno()
except Exception:
pass # Fall through to generic code.
else:
try:
# Use OS copy-on-write where available.
if _ficlone:
try:
_ficlone(source_fd, target_fd)
return
except OSError as err:
if err.errno not in (EBADF, EOPNOTSUPP, ETXTBSY, EXDEV):
raise err
# Use OS copy where available.
if _fcopyfile:
try:
_fcopyfile(source_fd, target_fd)
return
except OSError as err:
if err.errno not in (EINVAL, ENOTSUP):
raise err
if _copy_file_range:
try:
_copy_file_range(source_fd, target_fd)
return
except OSError as err:
if err.errno not in (ETXTBSY, EXDEV):
raise err
if _sendfile:
try:
_sendfile(source_fd, target_fd)
return
except OSError as err:
if err.errno != ENOTSOCK:
raise err
except OSError as err:
# Produce more useful error messages.
err.filename = source_f.name
err.filename2 = target_f.name
raise err
# Last resort: copy with fileobj read() and write().
read_source = source_f.read
write_target = target_f.write
while buf := read_source(1024 * 1024):
write_target(buf)
def magic_open(path, mode='r', buffering=-1, encoding=None, errors=None,
newline=None):
"""
Open the file pointed to by this path and return a file object, as
the built-in open() function does.
"""
text = 'b' not in mode
if text:
# Call io.text_encoding() here to ensure any warning is raised at an
# appropriate stack level.
encoding = text_encoding(encoding)
try:
return open(path, mode, buffering, encoding, errors, newline)
except TypeError:
pass
cls = type(path)
mode = ''.join(sorted(c for c in mode if c not in 'bt'))
if text:
try:
attr = getattr(cls, f'__open_{mode}__')
except AttributeError:
pass
else:
return attr(path, buffering, encoding, errors, newline)
elif encoding is not None:
raise ValueError("binary mode doesn't take an encoding argument")
elif errors is not None:
raise ValueError("binary mode doesn't take an errors argument")
elif newline is not None:
raise ValueError("binary mode doesn't take a newline argument")
try:
attr = getattr(cls, f'__open_{mode}b__')
except AttributeError:
pass
else:
stream = attr(path, buffering)
if text:
stream = TextIOWrapper(stream, encoding, errors, newline)
return stream
raise TypeError(f"{cls.__name__} can't be opened with mode {mode!r}")
def ensure_distinct_paths(source, target):
"""
Raise OSError(EINVAL) if the other path is within this path.
"""
# Note: there is no straightforward, foolproof algorithm to determine
# if one directory is within another (a particularly perverse example
# would be a single network share mounted in one location via NFS, and
# in another location via CIFS), so we simply checks whether the
# other path is lexically equal to, or within, this path.
if source == target:
err = OSError(EINVAL, "Source and target are the same path")
elif source in target.parents:
err = OSError(EINVAL, "Source path is a parent of target path")
else:
return
err.filename = str(source)
err.filename2 = str(target)
raise err
def ensure_different_files(source, target):
"""
Raise OSError(EINVAL) if both paths refer to the same file.
"""
try:
source_file_id = source.info._file_id
target_file_id = target.info._file_id
except AttributeError:
if source != target:
return
else:
try:
if source_file_id() != target_file_id():
return
except (OSError, ValueError):
return
err = OSError(EINVAL, "Source and target are the same file")
err.filename = str(source)
err.filename2 = str(target)
raise err
def copy_info(info, target, follow_symlinks=True):
"""Copy metadata from the given PathInfo to the given local path."""
copy_times_ns = (
hasattr(info, '_access_time_ns') and
hasattr(info, '_mod_time_ns') and
(follow_symlinks or os.utime in os.supports_follow_symlinks))
if copy_times_ns:
t0 = info._access_time_ns(follow_symlinks=follow_symlinks)
t1 = info._mod_time_ns(follow_symlinks=follow_symlinks)
os.utime(target, ns=(t0, t1), follow_symlinks=follow_symlinks)
# We must copy extended attributes before the file is (potentially)
# chmod()'ed read-only, otherwise setxattr() will error with -EACCES.
copy_xattrs = (
hasattr(info, '_xattrs') and
hasattr(os, 'setxattr') and
(follow_symlinks or os.setxattr in os.supports_follow_symlinks))
if copy_xattrs:
xattrs = info._xattrs(follow_symlinks=follow_symlinks)
for attr, value in xattrs:
try:
os.setxattr(target, attr, value, follow_symlinks=follow_symlinks)
except OSError as e:
if e.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
raise
copy_posix_permissions = (
hasattr(info, '_posix_permissions') and
(follow_symlinks or os.chmod in os.supports_follow_symlinks))
if copy_posix_permissions:
posix_permissions = info._posix_permissions(follow_symlinks=follow_symlinks)
try:
os.chmod(target, posix_permissions, follow_symlinks=follow_symlinks)
except NotImplementedError:
# if we got a NotImplementedError, it's because
# * follow_symlinks=False,
# * lchown() is unavailable, and
# * either
# * fchownat() is unavailable or
# * fchownat() doesn't implement AT_SYMLINK_NOFOLLOW.
# (it returned ENOSUP.)
# therefore we're out of options--we simply cannot chown the
# symlink. give up, suppress the error.
# (which is what shutil always did in this circumstance.)
pass
copy_bsd_flags = (
hasattr(info, '_bsd_flags') and
hasattr(os, 'chflags') and
(follow_symlinks or os.chflags in os.supports_follow_symlinks))
if copy_bsd_flags:
bsd_flags = info._bsd_flags(follow_symlinks=follow_symlinks)
try:
os.chflags(target, bsd_flags, follow_symlinks=follow_symlinks)
except OSError as why:
if why.errno not in (EOPNOTSUPP, ENOTSUP):
raise
class _PathInfoBase:
__slots__ = ('_path', '_stat_result', '_lstat_result')
def __init__(self, path):
self._path = str(path)
def __repr__(self):
path_type = "WindowsPath" if os.name == "nt" else "PosixPath"
return f"<{path_type}.info>"
def _stat(self, *, follow_symlinks=True, ignore_errors=False):
"""Return the status as an os.stat_result, or None if stat() fails and
ignore_errors is true."""
if follow_symlinks:
try:
result = self._stat_result
except AttributeError:
pass
else:
if ignore_errors or result is not None:
return result
try:
self._stat_result = os.stat(self._path)
except (OSError, ValueError):
self._stat_result = None
if not ignore_errors:
raise
return self._stat_result
else:
try:
result = self._lstat_result
except AttributeError:
pass
else:
if ignore_errors or result is not None:
return result
try:
self._lstat_result = os.lstat(self._path)
except (OSError, ValueError):
self._lstat_result = None
if not ignore_errors:
raise
return self._lstat_result
def _posix_permissions(self, *, follow_symlinks=True):
"""Return the POSIX file permissions."""
return S_IMODE(self._stat(follow_symlinks=follow_symlinks).st_mode)
def _file_id(self, *, follow_symlinks=True):
"""Returns the identifier of the file."""
st = self._stat(follow_symlinks=follow_symlinks)
return st.st_dev, st.st_ino
def _access_time_ns(self, *, follow_symlinks=True):
"""Return the access time in nanoseconds."""
return self._stat(follow_symlinks=follow_symlinks).st_atime_ns
def _mod_time_ns(self, *, follow_symlinks=True):
"""Return the modify time in nanoseconds."""
return self._stat(follow_symlinks=follow_symlinks).st_mtime_ns
if hasattr(os.stat_result, 'st_flags'):
def _bsd_flags(self, *, follow_symlinks=True):
"""Return the flags."""
return self._stat(follow_symlinks=follow_symlinks).st_flags
if hasattr(os, 'listxattr'):
def _xattrs(self, *, follow_symlinks=True):
"""Return the xattrs as a list of (attr, value) pairs, or an empty
list if extended attributes aren't supported."""
try:
return [
(attr, os.getxattr(self._path, attr, follow_symlinks=follow_symlinks))
for attr in os.listxattr(self._path, follow_symlinks=follow_symlinks)]
except OSError as err:
if err.errno not in (EPERM, ENOTSUP, ENODATA, EINVAL, EACCES):
raise
return []
class _WindowsPathInfo(_PathInfoBase):
"""Implementation of pathlib.types.PathInfo that provides status
information for Windows paths. Don't try to construct it yourself."""
__slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink')
def exists(self, *, follow_symlinks=True):
"""Whether this path exists."""
if not follow_symlinks and self.is_symlink():
return True
try:
return self._exists
except AttributeError:
if os.path.exists(self._path):
self._exists = True
return True
else:
self._exists = self._is_dir = self._is_file = False
return False
def is_dir(self, *, follow_symlinks=True):
"""Whether this path is a directory."""
if not follow_symlinks and self.is_symlink():
return False
try:
return self._is_dir
except AttributeError:
if os.path.isdir(self._path):
self._is_dir = self._exists = True
return True
else:
self._is_dir = False
return False
def is_file(self, *, follow_symlinks=True):
"""Whether this path is a regular file."""
if not follow_symlinks and self.is_symlink():
return False
try:
return self._is_file
except AttributeError:
if os.path.isfile(self._path):
self._is_file = self._exists = True
return True
else:
self._is_file = False
return False
def is_symlink(self):
"""Whether this path is a symbolic link."""
try:
return self._is_symlink
except AttributeError:
self._is_symlink = os.path.islink(self._path)
return self._is_symlink
class _PosixPathInfo(_PathInfoBase):
"""Implementation of pathlib.types.PathInfo that provides status
information for POSIX paths. Don't try to construct it yourself."""
__slots__ = ()
def exists(self, *, follow_symlinks=True):
"""Whether this path exists."""
st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True)
if st is None:
return False
return True
def is_dir(self, *, follow_symlinks=True):
"""Whether this path is a directory."""
st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True)
if st is None:
return False
return S_ISDIR(st.st_mode)
def is_file(self, *, follow_symlinks=True):
"""Whether this path is a regular file."""
st = self._stat(follow_symlinks=follow_symlinks, ignore_errors=True)
if st is None:
return False
return S_ISREG(st.st_mode)
def is_symlink(self):
"""Whether this path is a symbolic link."""
st = self._stat(follow_symlinks=False, ignore_errors=True)
if st is None:
return False
return S_ISLNK(st.st_mode)
PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo
class DirEntryInfo(_PathInfoBase):
"""Implementation of pathlib.types.PathInfo that provides status
information by querying a wrapped os.DirEntry object. Don't try to
construct it yourself."""
__slots__ = ('_entry',)
def __init__(self, entry):
super().__init__(entry.path)
self._entry = entry
def _stat(self, *, follow_symlinks=True, ignore_errors=False):
try:
return self._entry.stat(follow_symlinks=follow_symlinks)
except OSError:
if not ignore_errors:
raise
return None
def exists(self, *, follow_symlinks=True):
"""Whether this path exists."""
if not follow_symlinks:
return True
return self._stat(ignore_errors=True) is not None
def is_dir(self, *, follow_symlinks=True):
"""Whether this path is a directory."""
try:
return self._entry.is_dir(follow_symlinks=follow_symlinks)
except OSError:
return False
def is_file(self, *, follow_symlinks=True):
"""Whether this path is a regular file."""
try:
return self._entry.is_file(follow_symlinks=follow_symlinks)
except OSError:
return False
def is_symlink(self):
"""Whether this path is a symbolic link."""
try:
return self._entry.is_symlink()
except OSError:
return False
+430
View File
@@ -0,0 +1,430 @@
"""
Protocols for supporting classes in pathlib.
"""
# This module also provides abstract base classes for rich path objects.
# These ABCs are a *private* part of the Python standard library, but they're
# made available as a PyPI package called "pathlib-abc". It's possible they'll
# become an official part of the standard library in future.
#
# Three ABCs are provided -- _JoinablePath, _ReadablePath and _WritablePath
from abc import ABC, abstractmethod
from glob import _PathGlobber
from io import text_encoding
from pathlib._os import magic_open, ensure_distinct_paths, ensure_different_files, copyfileobj
from pathlib import PurePath, Path
from typing import Optional, Protocol, runtime_checkable
def _explode_path(path, split):
"""
Split the path into a 2-tuple (anchor, parts), where *anchor* is the
uppermost parent of the path (equivalent to path.parents[-1]), and
*parts* is a reversed list of parts following the anchor.
"""
parent, name = split(path)
names = []
while path != parent:
names.append(name)
path = parent
parent, name = split(path)
return path, names
@runtime_checkable
class _PathParser(Protocol):
"""Protocol for path parsers, which do low-level path manipulation.
Path parsers provide a subset of the os.path API, specifically those
functions needed to provide JoinablePath functionality. Each JoinablePath
subclass references its path parser via a 'parser' class attribute.
"""
sep: str
altsep: Optional[str]
def split(self, path: str) -> tuple[str, str]: ...
def splitext(self, path: str) -> tuple[str, str]: ...
def normcase(self, path: str) -> str: ...
@runtime_checkable
class PathInfo(Protocol):
"""Protocol for path info objects, which support querying the file type.
Methods may return cached results.
"""
def exists(self, *, follow_symlinks: bool = True) -> bool: ...
def is_dir(self, *, follow_symlinks: bool = True) -> bool: ...
def is_file(self, *, follow_symlinks: bool = True) -> bool: ...
def is_symlink(self) -> bool: ...
class _JoinablePath(ABC):
"""Abstract base class for pure path objects.
This class *does not* provide several magic methods that are defined in
its implementation PurePath. They are: __init__, __fspath__, __bytes__,
__reduce__, __hash__, __eq__, __lt__, __le__, __gt__, __ge__.
"""
__slots__ = ()
@property
@abstractmethod
def parser(self):
"""Implementation of pathlib._types.Parser used for low-level path
parsing and manipulation.
"""
raise NotImplementedError
@abstractmethod
def with_segments(self, *pathsegments):
"""Construct a new path object from any number of path-like objects.
Subclasses may override this method to customize how new path objects
are created from methods like `iterdir()`.
"""
raise NotImplementedError
@abstractmethod
def __str__(self):
"""Return the string representation of the path, suitable for
passing to system calls."""
raise NotImplementedError
@property
def anchor(self):
"""The concatenation of the drive and root, or ''."""
return _explode_path(str(self), self.parser.split)[0]
@property
def name(self):
"""The final path component, if any."""
return self.parser.split(str(self))[1]
@property
def suffix(self):
"""
The final component's last suffix, if any.
This includes the leading period. For example: '.txt'
"""
return self.parser.splitext(self.name)[1]
@property
def suffixes(self):
"""
A list of the final component's suffixes, if any.
These include the leading periods. For example: ['.tar', '.gz']
"""
split = self.parser.splitext
stem, suffix = split(self.name)
suffixes = []
while suffix:
suffixes.append(suffix)
stem, suffix = split(stem)
return suffixes[::-1]
@property
def stem(self):
"""The final path component, minus its last suffix."""
return self.parser.splitext(self.name)[0]
def with_name(self, name):
"""Return a new path with the file name changed."""
split = self.parser.split
if split(name)[0]:
raise ValueError(f"Invalid name {name!r}")
path = str(self)
path = path.removesuffix(split(path)[1]) + name
return self.with_segments(path)
def with_stem(self, stem):
"""Return a new path with the stem changed."""
suffix = self.suffix
if not suffix:
return self.with_name(stem)
elif not stem:
# If the suffix is non-empty, we can't make the stem empty.
raise ValueError(f"{self!r} has a non-empty suffix")
else:
return self.with_name(stem + suffix)
def with_suffix(self, suffix):
"""Return a new path with the file suffix changed. If the path
has no suffix, add given suffix. If the given suffix is an empty
string, remove the suffix from the path.
"""
stem = self.stem
if not stem:
# If the stem is empty, we can't make the suffix non-empty.
raise ValueError(f"{self!r} has an empty name")
elif suffix and not suffix.startswith('.'):
raise ValueError(f"Invalid suffix {suffix!r}")
else:
return self.with_name(stem + suffix)
@property
def parts(self):
"""An object providing sequence-like access to the
components in the filesystem path."""
anchor, parts = _explode_path(str(self), self.parser.split)
if anchor:
parts.append(anchor)
return tuple(reversed(parts))
def joinpath(self, *pathsegments):
"""Combine this path with one or several arguments, and return a
new path representing either a subpath (if all arguments are relative
paths) or a totally different path (if one of the arguments is
anchored).
"""
return self.with_segments(str(self), *pathsegments)
def __truediv__(self, key):
try:
return self.with_segments(str(self), key)
except TypeError:
return NotImplemented
def __rtruediv__(self, key):
try:
return self.with_segments(key, str(self))
except TypeError:
return NotImplemented
@property
def parent(self):
"""The logical parent of the path."""
path = str(self)
parent = self.parser.split(path)[0]
if path != parent:
return self.with_segments(parent)
return self
@property
def parents(self):
"""A sequence of this path's logical parents."""
split = self.parser.split
path = str(self)
parent = split(path)[0]
parents = []
while path != parent:
parents.append(self.with_segments(parent))
path = parent
parent = split(path)[0]
return tuple(parents)
def full_match(self, pattern):
"""
Return True if this path matches the given glob-style pattern. The
pattern is matched against the entire path.
"""
case_sensitive = self.parser.normcase('Aa') == 'Aa'
globber = _PathGlobber(self.parser.sep, case_sensitive, recursive=True)
match = globber.compile(pattern, altsep=self.parser.altsep)
return match(str(self)) is not None
class _ReadablePath(_JoinablePath):
"""Abstract base class for readable path objects.
The Path class implements this ABC for local filesystem paths. Users may
create subclasses to implement readable virtual filesystem paths, such as
paths in archive files or on remote storage systems.
"""
__slots__ = ()
@property
@abstractmethod
def info(self):
"""
A PathInfo object that exposes the file type and other file attributes
of this path.
"""
raise NotImplementedError
@abstractmethod
def __open_rb__(self, buffering=-1):
"""
Open the file pointed to by this path for reading in binary mode and
return a file object, like open(mode='rb').
"""
raise NotImplementedError
def read_bytes(self):
"""
Open the file in bytes mode, read it, and close the file.
"""
with magic_open(self, mode='rb', buffering=0) as f:
return f.read()
def read_text(self, encoding=None, errors=None, newline=None):
"""
Open the file in text mode, read it, and close the file.
"""
# Call io.text_encoding() here to ensure any warning is raised at an
# appropriate stack level.
encoding = text_encoding(encoding)
with magic_open(self, mode='r', encoding=encoding, errors=errors, newline=newline) as f:
return f.read()
@abstractmethod
def iterdir(self):
"""Yield path objects of the directory contents.
The children are yielded in arbitrary order, and the
special entries '.' and '..' are not included.
"""
raise NotImplementedError
def glob(self, pattern, *, recurse_symlinks=True):
"""Iterate over this subtree and yield all existing files (of any
kind, including directories) matching the given relative pattern.
"""
anchor, parts = _explode_path(pattern, self.parser.split)
if anchor:
raise NotImplementedError("Non-relative patterns are unsupported")
elif not parts:
raise ValueError(f"Unacceptable pattern: {pattern!r}")
elif not recurse_symlinks:
raise NotImplementedError("recurse_symlinks=False is unsupported")
case_sensitive = self.parser.normcase('Aa') == 'Aa'
globber = _PathGlobber(self.parser.sep, case_sensitive, recursive=True)
select = globber.selector(parts)
return select(self.joinpath(''))
def walk(self, top_down=True, on_error=None, follow_symlinks=False):
"""Walk the directory tree from this directory, similar to os.walk()."""
paths = [self]
while paths:
path = paths.pop()
if isinstance(path, tuple):
yield path
continue
dirnames = []
filenames = []
if not top_down:
paths.append((path, dirnames, filenames))
try:
for child in path.iterdir():
if child.info.is_dir(follow_symlinks=follow_symlinks):
if not top_down:
paths.append(child)
dirnames.append(child.name)
else:
filenames.append(child.name)
except OSError as error:
if on_error is not None:
on_error(error)
if not top_down:
while not isinstance(paths.pop(), tuple):
pass
continue
if top_down:
yield path, dirnames, filenames
paths += [path.joinpath(d) for d in reversed(dirnames)]
@abstractmethod
def readlink(self):
"""
Return the path to which the symbolic link points.
"""
raise NotImplementedError
def copy(self, target, **kwargs):
"""
Recursively copy this file or directory tree to the given destination.
"""
ensure_distinct_paths(self, target)
target._copy_from(self, **kwargs)
return target.joinpath() # Empty join to ensure fresh metadata.
def copy_into(self, target_dir, **kwargs):
"""
Copy this file or directory tree into the given existing directory.
"""
name = self.name
if not name:
raise ValueError(f"{self!r} has an empty name")
return self.copy(target_dir / name, **kwargs)
class _WritablePath(_JoinablePath):
"""Abstract base class for writable path objects.
The Path class implements this ABC for local filesystem paths. Users may
create subclasses to implement writable virtual filesystem paths, such as
paths in archive files or on remote storage systems.
"""
__slots__ = ()
@abstractmethod
def symlink_to(self, target, target_is_directory=False):
"""
Make this path a symlink pointing to the target path.
Note the order of arguments (link, target) is the reverse of os.symlink.
"""
raise NotImplementedError
@abstractmethod
def mkdir(self):
"""
Create a new directory at this given path.
"""
raise NotImplementedError
@abstractmethod
def __open_wb__(self, buffering=-1):
"""
Open the file pointed to by this path for writing in binary mode and
return a file object, like open(mode='wb').
"""
raise NotImplementedError
def write_bytes(self, data):
"""
Open the file in bytes mode, write to it, and close the file.
"""
# type-check for the buffer interface before truncating the file
view = memoryview(data)
with magic_open(self, mode='wb') as f:
return f.write(view)
def write_text(self, data, encoding=None, errors=None, newline=None):
"""
Open the file in text mode, write to it, and close the file.
"""
# Call io.text_encoding() here to ensure any warning is raised at an
# appropriate stack level.
encoding = text_encoding(encoding)
if not isinstance(data, str):
raise TypeError('data must be str, not %s' %
data.__class__.__name__)
with magic_open(self, mode='w', encoding=encoding, errors=errors, newline=newline) as f:
return f.write(data)
def _copy_from(self, source, follow_symlinks=True):
"""
Recursively copy the given path to this path.
"""
stack = [(source, self)]
while stack:
src, dst = stack.pop()
if not follow_symlinks and src.info.is_symlink():
dst.symlink_to(str(src.readlink()), src.info.is_dir())
elif src.info.is_dir():
children = src.iterdir()
dst.mkdir()
for child in children:
stack.append((child, dst.joinpath(child.name)))
else:
ensure_different_files(src, dst)
with magic_open(src, 'rb') as source_f:
with magic_open(dst, 'wb') as target_f:
copyfileobj(source_f, target_f)
_JoinablePath.register(PurePath)
_ReadablePath.register(Path)
_WritablePath.register(Path)
+141 -52
View File
@@ -1,11 +1,15 @@
"""Test cases for the fnmatch module."""
import unittest
import os
import string
import unittest
import warnings
from fnmatch import fnmatch, fnmatchcase, translate, filter, filterfalse
IGNORECASE = os.path.normcase('P') == os.path.normcase('p')
NORMSEP = os.path.normcase('\\') == os.path.normcase('/')
from fnmatch import fnmatch, fnmatchcase, translate, filter
class FnmatchTestCase(unittest.TestCase):
@@ -77,23 +81,20 @@ class FnmatchTestCase(unittest.TestCase):
self.check_match(b'foo\nbar', b'foo*')
def test_case(self):
ignorecase = os.path.normcase('ABC') == os.path.normcase('abc')
check = self.check_match
check('abc', 'abc')
check('AbC', 'abc', ignorecase)
check('abc', 'AbC', ignorecase)
check('AbC', 'abc', IGNORECASE)
check('abc', 'AbC', IGNORECASE)
check('AbC', 'AbC')
def test_sep(self):
normsep = os.path.normcase('\\') == os.path.normcase('/')
check = self.check_match
check('usr/bin', 'usr/bin')
check('usr\\bin', 'usr/bin', normsep)
check('usr/bin', 'usr\\bin', normsep)
check('usr\\bin', 'usr/bin', NORMSEP)
check('usr/bin', 'usr\\bin', NORMSEP)
check('usr\\bin', 'usr\\bin')
def test_char_set(self):
ignorecase = os.path.normcase('ABC') == os.path.normcase('abc')
check = self.check_match
tescases = string.ascii_lowercase + string.digits + string.punctuation
for c in tescases:
@@ -101,11 +102,11 @@ class FnmatchTestCase(unittest.TestCase):
check(c, '[!az]', c not in 'az')
# Case insensitive.
for c in tescases:
check(c, '[AZ]', (c in 'az') and ignorecase)
check(c, '[!AZ]', (c not in 'az') or not ignorecase)
check(c, '[AZ]', (c in 'az') and IGNORECASE)
check(c, '[!AZ]', (c not in 'az') or not IGNORECASE)
for c in string.ascii_uppercase:
check(c, '[az]', (c in 'AZ') and ignorecase)
check(c, '[!az]', (c not in 'AZ') or not ignorecase)
check(c, '[az]', (c in 'AZ') and IGNORECASE)
check(c, '[!az]', (c not in 'AZ') or not IGNORECASE)
# Repeated same character.
for c in tescases:
check(c, '[aa]', c == 'a')
@@ -120,8 +121,6 @@ class FnmatchTestCase(unittest.TestCase):
check('[!]', '[!]')
def test_range(self):
ignorecase = os.path.normcase('ABC') == os.path.normcase('abc')
normsep = os.path.normcase('\\') == os.path.normcase('/')
check = self.check_match
tescases = string.ascii_lowercase + string.digits + string.punctuation
for c in tescases:
@@ -131,11 +130,11 @@ class FnmatchTestCase(unittest.TestCase):
check(c, '[!b-dx-z]', c not in 'bcdxyz')
# Case insensitive.
for c in tescases:
check(c, '[B-D]', (c in 'bcd') and ignorecase)
check(c, '[!B-D]', (c not in 'bcd') or not ignorecase)
check(c, '[B-D]', (c in 'bcd') and IGNORECASE)
check(c, '[!B-D]', (c not in 'bcd') or not IGNORECASE)
for c in string.ascii_uppercase:
check(c, '[b-d]', (c in 'BCD') and ignorecase)
check(c, '[!b-d]', (c not in 'BCD') or not ignorecase)
check(c, '[b-d]', (c in 'BCD') and IGNORECASE)
check(c, '[!b-d]', (c not in 'BCD') or not IGNORECASE)
# Upper bound == lower bound.
for c in tescases:
check(c, '[b-b]', c == 'b')
@@ -144,7 +143,7 @@ class FnmatchTestCase(unittest.TestCase):
check(c, '[!-#]', c not in '-#')
check(c, '[!--.]', c not in '-.')
check(c, '[^-`]', c in '^_`')
if not (normsep and c == '/'):
if not (NORMSEP and c == '/'):
check(c, '[[-^]', c in r'[\]^')
check(c, r'[\-^]', c in r'\]^')
check(c, '[b-]', c in '-b')
@@ -160,47 +159,45 @@ class FnmatchTestCase(unittest.TestCase):
check(c, '[d-bx-z]', c in 'xyz')
check(c, '[!d-bx-z]', c not in 'xyz')
check(c, '[d-b^-`]', c in '^_`')
if not (normsep and c == '/'):
if not (NORMSEP and c == '/'):
check(c, '[d-b[-^]', c in r'[\]^')
def test_sep_in_char_set(self):
normsep = os.path.normcase('\\') == os.path.normcase('/')
check = self.check_match
check('/', r'[/]')
check('\\', r'[\]')
check('/', r'[\]', normsep)
check('\\', r'[/]', normsep)
check('/', r'[\]', NORMSEP)
check('\\', r'[/]', NORMSEP)
check('[/]', r'[/]', False)
check(r'[\\]', r'[/]', False)
check('\\', r'[\t]')
check('/', r'[\t]', normsep)
check('/', r'[\t]', NORMSEP)
check('t', r'[\t]')
check('\t', r'[\t]', False)
def test_sep_in_range(self):
normsep = os.path.normcase('\\') == os.path.normcase('/')
check = self.check_match
check('a/b', 'a[.-0]b', not normsep)
check('a/b', 'a[.-0]b', not NORMSEP)
check('a\\b', 'a[.-0]b', False)
check('a\\b', 'a[Z-^]b', not normsep)
check('a\\b', 'a[Z-^]b', not NORMSEP)
check('a/b', 'a[Z-^]b', False)
check('a/b', 'a[/-0]b', not normsep)
check('a/b', 'a[/-0]b', not NORMSEP)
check(r'a\b', 'a[/-0]b', False)
check('a[/-0]b', 'a[/-0]b', False)
check(r'a[\-0]b', 'a[/-0]b', False)
check('a/b', 'a[.-/]b')
check(r'a\b', 'a[.-/]b', normsep)
check(r'a\b', 'a[.-/]b', NORMSEP)
check('a[.-/]b', 'a[.-/]b', False)
check(r'a[.-\]b', 'a[.-/]b', False)
check(r'a\b', r'a[\-^]b')
check('a/b', r'a[\-^]b', normsep)
check('a/b', r'a[\-^]b', NORMSEP)
check(r'a[\-^]b', r'a[\-^]b', False)
check('a[/-^]b', r'a[\-^]b', False)
check(r'a\b', r'a[Z-\]b', not normsep)
check(r'a\b', r'a[Z-\]b', not NORMSEP)
check('a/b', r'a[Z-\]b', False)
check(r'a[Z-\]b', r'a[Z-\]b', False)
check('a[Z-/]b', r'a[Z-\]b', False)
@@ -221,24 +218,24 @@ class TranslateTestCase(unittest.TestCase):
def test_translate(self):
import re
self.assertEqual(translate('*'), r'(?s:.*)\Z')
self.assertEqual(translate('?'), r'(?s:.)\Z')
self.assertEqual(translate('a?b*'), r'(?s:a.b.*)\Z')
self.assertEqual(translate('[abc]'), r'(?s:[abc])\Z')
self.assertEqual(translate('[]]'), r'(?s:[]])\Z')
self.assertEqual(translate('[!x]'), r'(?s:[^x])\Z')
self.assertEqual(translate('[^x]'), r'(?s:[\^x])\Z')
self.assertEqual(translate('[x'), r'(?s:\[x)\Z')
self.assertEqual(translate('*'), r'(?s:.*)\z')
self.assertEqual(translate('?'), r'(?s:.)\z')
self.assertEqual(translate('a?b*'), r'(?s:a.b.*)\z')
self.assertEqual(translate('[abc]'), r'(?s:[abc])\z')
self.assertEqual(translate('[]]'), r'(?s:[]])\z')
self.assertEqual(translate('[!x]'), r'(?s:[^x])\z')
self.assertEqual(translate('[^x]'), r'(?s:[\^x])\z')
self.assertEqual(translate('[x'), r'(?s:\[x)\z')
# from the docs
self.assertEqual(translate('*.txt'), r'(?s:.*\.txt)\Z')
self.assertEqual(translate('*.txt'), r'(?s:.*\.txt)\z')
# squash consecutive stars
self.assertEqual(translate('*********'), r'(?s:.*)\Z')
self.assertEqual(translate('A*********'), r'(?s:A.*)\Z')
self.assertEqual(translate('*********A'), r'(?s:.*A)\Z')
self.assertEqual(translate('A*********?[?]?'), r'(?s:A.*.[?].)\Z')
self.assertEqual(translate('*********'), r'(?s:.*)\z')
self.assertEqual(translate('A*********'), r'(?s:A.*)\z')
self.assertEqual(translate('*********A'), r'(?s:.*A)\z')
self.assertEqual(translate('A*********?[?]?'), r'(?s:A.*.[?].)\z')
# fancy translation to prevent exponential-time match failure
t = translate('**a*a****a')
self.assertEqual(t, r'(?s:(?>.*?a)(?>.*?a).*a)\Z')
self.assertEqual(t, r'(?s:(?>.*?a)(?>.*?a).*a)\z')
# and try pasting multiple translate results - it's an undocumented
# feature that this works
r1 = translate('**a**a**a*')
@@ -250,6 +247,75 @@ class TranslateTestCase(unittest.TestCase):
self.assertTrue(re.match(fatre, 'cbabcaxc'))
self.assertFalse(re.match(fatre, 'dabccbad'))
def test_translate_wildcards(self):
for pattern, expect in [
('ab*', r'(?s:ab.*)\z'),
('ab*cd', r'(?s:ab.*cd)\z'),
('ab*cd*', r'(?s:ab(?>.*?cd).*)\z'),
('ab*cd*12', r'(?s:ab(?>.*?cd).*12)\z'),
('ab*cd*12*', r'(?s:ab(?>.*?cd)(?>.*?12).*)\z'),
('ab*cd*12*34', r'(?s:ab(?>.*?cd)(?>.*?12).*34)\z'),
('ab*cd*12*34*', r'(?s:ab(?>.*?cd)(?>.*?12)(?>.*?34).*)\z'),
]:
with self.subTest(pattern):
translated = translate(pattern)
self.assertEqual(translated, expect, pattern)
for pattern, expect in [
('*ab', r'(?s:.*ab)\z'),
('*ab*', r'(?s:(?>.*?ab).*)\z'),
('*ab*cd', r'(?s:(?>.*?ab).*cd)\z'),
('*ab*cd*', r'(?s:(?>.*?ab)(?>.*?cd).*)\z'),
('*ab*cd*12', r'(?s:(?>.*?ab)(?>.*?cd).*12)\z'),
('*ab*cd*12*', r'(?s:(?>.*?ab)(?>.*?cd)(?>.*?12).*)\z'),
('*ab*cd*12*34', r'(?s:(?>.*?ab)(?>.*?cd)(?>.*?12).*34)\z'),
('*ab*cd*12*34*', r'(?s:(?>.*?ab)(?>.*?cd)(?>.*?12)(?>.*?34).*)\z'),
]:
with self.subTest(pattern):
translated = translate(pattern)
self.assertEqual(translated, expect, pattern)
def test_translate_expressions(self):
for pattern, expect in [
('[', r'(?s:\[)\z'),
('[!', r'(?s:\[!)\z'),
('[]', r'(?s:\[\])\z'),
('[abc', r'(?s:\[abc)\z'),
('[!abc', r'(?s:\[!abc)\z'),
('[abc]', r'(?s:[abc])\z'),
('[!abc]', r'(?s:[^abc])\z'),
('[!abc][!def]', r'(?s:[^abc][^def])\z'),
# with [[
('[[', r'(?s:\[\[)\z'),
('[[a', r'(?s:\[\[a)\z'),
('[[]', r'(?s:[\[])\z'),
('[[]a', r'(?s:[\[]a)\z'),
('[[]]', r'(?s:[\[]\])\z'),
('[[]a]', r'(?s:[\[]a\])\z'),
('[[a]', r'(?s:[\[a])\z'),
('[[a]]', r'(?s:[\[a]\])\z'),
('[[a]b', r'(?s:[\[a]b)\z'),
# backslashes
('[\\', r'(?s:\[\\)\z'),
(r'[\]', r'(?s:[\\])\z'),
(r'[\\]', r'(?s:[\\\\])\z'),
]:
with self.subTest(pattern):
translated = translate(pattern)
self.assertEqual(translated, expect, pattern)
def test_star_indices_locations(self):
from fnmatch import _translate
blocks = ['a^b', '***', '?', '?', '[a-z]', '[1-9]', '*', '++', '[[a']
parts, star_indices = _translate(''.join(blocks), '*', '.')
expect_parts = ['a', r'\^', 'b', '*',
'.', '.', '[a-z]', '[1-9]', '*',
r'\+', r'\+', r'\[', r'\[', 'a']
self.assertListEqual(parts, expect_parts)
self.assertListEqual(star_indices, [3, 8])
class FilterTestCase(unittest.TestCase):
def test_filter(self):
@@ -263,18 +329,41 @@ class FilterTestCase(unittest.TestCase):
self.assertRaises(TypeError, filter, [b'test'], '*')
def test_case(self):
ignorecase = os.path.normcase('P') == os.path.normcase('p')
self.assertEqual(filter(['Test.py', 'Test.rb', 'Test.PL'], '*.p*'),
['Test.py', 'Test.PL'] if ignorecase else ['Test.py'])
['Test.py', 'Test.PL'] if IGNORECASE else ['Test.py'])
self.assertEqual(filter(['Test.py', 'Test.rb', 'Test.PL'], '*.P*'),
['Test.py', 'Test.PL'] if ignorecase else ['Test.PL'])
['Test.py', 'Test.PL'] if IGNORECASE else ['Test.PL'])
def test_sep(self):
normsep = os.path.normcase('\\') == os.path.normcase('/')
self.assertEqual(filter(['usr/bin', 'usr', 'usr\\lib'], 'usr/*'),
['usr/bin', 'usr\\lib'] if normsep else ['usr/bin'])
['usr/bin', 'usr\\lib'] if NORMSEP else ['usr/bin'])
self.assertEqual(filter(['usr/bin', 'usr', 'usr\\lib'], 'usr\\*'),
['usr/bin', 'usr\\lib'] if normsep else ['usr\\lib'])
['usr/bin', 'usr\\lib'] if NORMSEP else ['usr\\lib'])
class FilterFalseTestCase(unittest.TestCase):
def test_filterfalse(self):
actual = filterfalse(['Python', 'Ruby', 'Perl', 'Tcl'], 'P*')
self.assertListEqual(actual, ['Ruby', 'Tcl'])
actual = filterfalse([b'Python', b'Ruby', b'Perl', b'Tcl'], b'P*')
self.assertListEqual(actual, [b'Ruby', b'Tcl'])
def test_mix_bytes_str(self):
self.assertRaises(TypeError, filterfalse, ['test'], b'*')
self.assertRaises(TypeError, filterfalse, [b'test'], '*')
def test_case(self):
self.assertEqual(filterfalse(['Test.py', 'Test.rb', 'Test.PL'], '*.p*'),
['Test.rb'] if IGNORECASE else ['Test.rb', 'Test.PL'])
self.assertEqual(filterfalse(['Test.py', 'Test.rb', 'Test.PL'], '*.P*'),
['Test.rb'] if IGNORECASE else ['Test.py', 'Test.rb',])
def test_sep(self):
self.assertEqual(filterfalse(['usr/bin', 'usr', 'usr\\lib'], 'usr/*'),
['usr'] if NORMSEP else ['usr', 'usr\\lib'])
self.assertEqual(filterfalse(['usr/bin', 'usr', 'usr\\lib'], 'usr\\*'),
['usr'] if NORMSEP else ['usr/bin', 'usr'])
if __name__ == "__main__":
+40 -92
View File
@@ -459,111 +459,59 @@ class GlobTests(unittest.TestCase):
def test_translate(self):
def fn(pat):
return glob.translate(pat, seps='/')
self.assertEqual(fn('foo'), r'(?s:foo)\Z')
self.assertEqual(fn('foo/bar'), r'(?s:foo/bar)\Z')
self.assertEqual(fn('*'), r'(?s:[^/.][^/]*)\Z')
self.assertEqual(fn('?'), r'(?s:(?!\.)[^/])\Z')
self.assertEqual(fn('a*'), r'(?s:a[^/]*)\Z')
self.assertEqual(fn('*a'), r'(?s:(?!\.)[^/]*a)\Z')
self.assertEqual(fn('.*'), r'(?s:\.[^/]*)\Z')
self.assertEqual(fn('?aa'), r'(?s:(?!\.)[^/]aa)\Z')
self.assertEqual(fn('aa?'), r'(?s:aa[^/])\Z')
self.assertEqual(fn('aa[ab]'), r'(?s:aa[ab])\Z')
self.assertEqual(fn('**'), r'(?s:(?!\.)[^/]*)\Z')
self.assertEqual(fn('***'), r'(?s:(?!\.)[^/]*)\Z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\Z')
self.assertEqual(fn('**b'), r'(?s:(?!\.)[^/]*b)\Z')
self.assertEqual(fn('foo'), r'(?s:foo)\z')
self.assertEqual(fn('foo/bar'), r'(?s:foo/bar)\z')
self.assertEqual(fn('*'), r'(?s:[^/.][^/]*)\z')
self.assertEqual(fn('?'), r'(?s:(?!\.)[^/])\z')
self.assertEqual(fn('a*'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('*a'), r'(?s:(?!\.)[^/]*a)\z')
self.assertEqual(fn('.*'), r'(?s:\.[^/]*)\z')
self.assertEqual(fn('?aa'), r'(?s:(?!\.)[^/]aa)\z')
self.assertEqual(fn('aa?'), r'(?s:aa[^/])\z')
self.assertEqual(fn('aa[ab]'), r'(?s:aa[ab])\z')
self.assertEqual(fn('**'), r'(?s:(?!\.)[^/]*)\z')
self.assertEqual(fn('***'), r'(?s:(?!\.)[^/]*)\z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('**b'), r'(?s:(?!\.)[^/]*b)\z')
self.assertEqual(fn('/**/*/*.*/**'),
r'(?s:/(?!\.)[^/]*/[^/.][^/]*/(?!\.)[^/]*\.[^/]*/(?!\.)[^/]*)\Z')
r'(?s:/(?!\.)[^/]*/[^/.][^/]*/(?!\.)[^/]*\.[^/]*/(?!\.)[^/]*)\z')
def test_translate_include_hidden(self):
def fn(pat):
return glob.translate(pat, include_hidden=True, seps='/')
self.assertEqual(fn('foo'), r'(?s:foo)\Z')
self.assertEqual(fn('foo/bar'), r'(?s:foo/bar)\Z')
self.assertEqual(fn('*'), r'(?s:[^/]+)\Z')
self.assertEqual(fn('?'), r'(?s:[^/])\Z')
self.assertEqual(fn('a*'), r'(?s:a[^/]*)\Z')
self.assertEqual(fn('*a'), r'(?s:[^/]*a)\Z')
self.assertEqual(fn('.*'), r'(?s:\.[^/]*)\Z')
self.assertEqual(fn('?aa'), r'(?s:[^/]aa)\Z')
self.assertEqual(fn('aa?'), r'(?s:aa[^/])\Z')
self.assertEqual(fn('aa[ab]'), r'(?s:aa[ab])\Z')
self.assertEqual(fn('**'), r'(?s:[^/]*)\Z')
self.assertEqual(fn('***'), r'(?s:[^/]*)\Z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\Z')
self.assertEqual(fn('**b'), r'(?s:[^/]*b)\Z')
self.assertEqual(fn('/**/*/*.*/**'), r'(?s:/[^/]*/[^/]+/[^/]*\.[^/]*/[^/]*)\Z')
self.assertEqual(fn('foo'), r'(?s:foo)\z')
self.assertEqual(fn('foo/bar'), r'(?s:foo/bar)\z')
self.assertEqual(fn('*'), r'(?s:[^/]+)\z')
self.assertEqual(fn('?'), r'(?s:[^/])\z')
self.assertEqual(fn('a*'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('*a'), r'(?s:[^/]*a)\z')
self.assertEqual(fn('.*'), r'(?s:\.[^/]*)\z')
self.assertEqual(fn('?aa'), r'(?s:[^/]aa)\z')
self.assertEqual(fn('aa?'), r'(?s:aa[^/])\z')
self.assertEqual(fn('aa[ab]'), r'(?s:aa[ab])\z')
self.assertEqual(fn('**'), r'(?s:[^/]*)\z')
self.assertEqual(fn('***'), r'(?s:[^/]*)\z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('**b'), r'(?s:[^/]*b)\z')
self.assertEqual(fn('/**/*/*.*/**'), r'(?s:/[^/]*/[^/]+/[^/]*\.[^/]*/[^/]*)\z')
def test_translate_recursive(self):
def fn(pat):
return glob.translate(pat, recursive=True, include_hidden=True, seps='/')
self.assertEqual(fn('*'), r'(?s:[^/]+)\Z')
self.assertEqual(fn('?'), r'(?s:[^/])\Z')
self.assertEqual(fn('**'), r'(?s:.*)\Z')
self.assertEqual(fn('**/**'), r'(?s:.*)\Z')
self.assertEqual(fn('***'), r'(?s:[^/]*)\Z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\Z')
self.assertEqual(fn('**b'), r'(?s:[^/]*b)\Z')
self.assertEqual(fn('/**/*/*.*/**'), r'(?s:/(?:.+/)?[^/]+/[^/]*\.[^/]*/.*)\Z')
self.assertEqual(fn('*'), r'(?s:[^/]+)\z')
self.assertEqual(fn('?'), r'(?s:[^/])\z')
self.assertEqual(fn('**'), r'(?s:.*)\z')
self.assertEqual(fn('**/**'), r'(?s:.*)\z')
self.assertEqual(fn('***'), r'(?s:[^/]*)\z')
self.assertEqual(fn('a**'), r'(?s:a[^/]*)\z')
self.assertEqual(fn('**b'), r'(?s:[^/]*b)\z')
self.assertEqual(fn('/**/*/*.*/**'), r'(?s:/(?:.+/)?[^/]+/[^/]*\.[^/]*/.*)\z')
def test_translate_seps(self):
def fn(pat):
return glob.translate(pat, recursive=True, include_hidden=True, seps=['/', '\\'])
self.assertEqual(fn('foo/bar\\baz'), r'(?s:foo[/\\]bar[/\\]baz)\Z')
self.assertEqual(fn('**/*'), r'(?s:(?:.+[/\\])?[^/\\]+)\Z')
@skip_unless_symlink
class SymlinkLoopGlobTests(unittest.TestCase):
# gh-109959: On Linux, glob._isdir() and glob._lexists() can return False
# randomly when checking the "link/" symbolic link.
# https://github.com/python/cpython/issues/109959#issuecomment-2577550700
@unittest.skip("flaky test")
def test_selflink(self):
tempdir = TESTFN + "_dir"
os.makedirs(tempdir)
self.addCleanup(shutil.rmtree, tempdir)
with change_cwd(tempdir):
os.makedirs('dir')
create_empty_file(os.path.join('dir', 'file'))
os.symlink(os.curdir, os.path.join('dir', 'link'))
results = glob.glob('**', recursive=True)
self.assertEqual(len(results), len(set(results)))
results = set(results)
depth = 0
while results:
path = os.path.join(*(['dir'] + ['link'] * depth))
self.assertIn(path, results)
results.remove(path)
if not results:
break
path = os.path.join(path, 'file')
self.assertIn(path, results)
results.remove(path)
depth += 1
results = glob.glob(os.path.join('**', 'file'), recursive=True)
self.assertEqual(len(results), len(set(results)))
results = set(results)
depth = 0
while results:
path = os.path.join(*(['dir'] + ['link'] * depth + ['file']))
self.assertIn(path, results)
results.remove(path)
depth += 1
results = glob.glob(os.path.join('**', ''), recursive=True)
self.assertEqual(len(results), len(set(results)))
results = set(results)
depth = 0
while results:
path = os.path.join(*(['dir'] + ['link'] * depth + ['']))
self.assertIn(path, results)
results.remove(path)
depth += 1
self.assertEqual(fn('foo/bar\\baz'), r'(?s:foo[/\\]bar[/\\]baz)\z')
self.assertEqual(fn('**/*'), r'(?s:(?:.+[/\\])?[^/\\]+)\z')
if __name__ == "__main__":
+2
View File
@@ -0,0 +1,2 @@
# Set to 'True' if the tests are run against the pathlib-abc PyPI package.
is_pypi = False
+51
View File
@@ -0,0 +1,51 @@
"""
Simple implementation of JoinablePath, for use in pathlib tests.
"""
import ntpath
import os.path
import posixpath
from . import is_pypi
if is_pypi:
from pathlib_abc import _JoinablePath
else:
from pathlib.types import _JoinablePath
class LexicalPath(_JoinablePath):
__slots__ = ('_segments',)
parser = os.path
def __init__(self, *pathsegments):
self._segments = pathsegments
def __hash__(self):
return hash(str(self))
def __eq__(self, other):
if not isinstance(other, LexicalPath):
return NotImplemented
return str(self) == str(other)
def __str__(self):
if not self._segments:
return ''
return self.parser.join(*self._segments)
def __repr__(self):
return f'{type(self).__name__}({str(self)!r})'
def with_segments(self, *pathsegments):
return type(self)(*pathsegments)
class LexicalPosixPath(LexicalPath):
__slots__ = ()
parser = posixpath
class LexicalWindowsPath(LexicalPath):
__slots__ = ()
parser = ntpath
+177
View File
@@ -0,0 +1,177 @@
"""
Implementations of ReadablePath and WritablePath for local paths, for use in
pathlib tests.
LocalPathGround is also defined here. It helps establish the "ground truth"
about local paths in tests.
"""
import os
from . import is_pypi
from .lexical_path import LexicalPath
if is_pypi:
from shutil import rmtree
from pathlib_abc import PathInfo, _ReadablePath, _WritablePath
can_symlink = True
testfn = "TESTFN"
else:
from pathlib.types import PathInfo, _ReadablePath, _WritablePath
from test.support import os_helper
can_symlink = os_helper.can_symlink()
testfn = os_helper.TESTFN
rmtree = os_helper.rmtree
class LocalPathGround:
can_symlink = can_symlink
def __init__(self, path_cls):
self.path_cls = path_cls
def setup(self, local_suffix=""):
root = self.path_cls(testfn + local_suffix)
os.mkdir(root)
return root
def teardown(self, root):
rmtree(root)
def create_file(self, p, data=b''):
with open(p, 'wb') as f:
f.write(data)
def create_dir(self, p):
os.mkdir(p)
def create_symlink(self, p, target):
os.symlink(target, p)
def create_hierarchy(self, p):
os.mkdir(os.path.join(p, 'dirA'))
os.mkdir(os.path.join(p, 'dirB'))
os.mkdir(os.path.join(p, 'dirC'))
os.mkdir(os.path.join(p, 'dirC', 'dirD'))
with open(os.path.join(p, 'fileA'), 'wb') as f:
f.write(b"this is file A\n")
with open(os.path.join(p, 'dirB', 'fileB'), 'wb') as f:
f.write(b"this is file B\n")
with open(os.path.join(p, 'dirC', 'fileC'), 'wb') as f:
f.write(b"this is file C\n")
with open(os.path.join(p, 'dirC', 'novel.txt'), 'wb') as f:
f.write(b"this is a novel\n")
with open(os.path.join(p, 'dirC', 'dirD', 'fileD'), 'wb') as f:
f.write(b"this is file D\n")
if self.can_symlink:
# Relative symlinks.
os.symlink('fileA', os.path.join(p, 'linkA'))
os.symlink('non-existing', os.path.join(p, 'brokenLink'))
os.symlink('dirB',
os.path.join(p, 'linkB'),
target_is_directory=True)
os.symlink(os.path.join('..', 'dirB'),
os.path.join(p, 'dirA', 'linkC'),
target_is_directory=True)
# Broken symlink (pointing to itself).
os.symlink('brokenLinkLoop', os.path.join(p, 'brokenLinkLoop'))
isdir = staticmethod(os.path.isdir)
isfile = staticmethod(os.path.isfile)
islink = staticmethod(os.path.islink)
readlink = staticmethod(os.readlink)
def readtext(self, p):
with open(p, 'r', encoding='utf-8') as f:
return f.read()
def readbytes(self, p):
with open(p, 'rb') as f:
return f.read()
class LocalPathInfo(PathInfo):
"""
Simple implementation of PathInfo for a local path
"""
__slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink')
def __init__(self, path):
self._path = str(path)
self._exists = None
self._is_dir = None
self._is_file = None
self._is_symlink = None
def exists(self, *, follow_symlinks=True):
"""Whether this path exists."""
if not follow_symlinks and self.is_symlink():
return True
if self._exists is None:
self._exists = os.path.exists(self._path)
return self._exists
def is_dir(self, *, follow_symlinks=True):
"""Whether this path is a directory."""
if not follow_symlinks and self.is_symlink():
return False
if self._is_dir is None:
self._is_dir = os.path.isdir(self._path)
return self._is_dir
def is_file(self, *, follow_symlinks=True):
"""Whether this path is a regular file."""
if not follow_symlinks and self.is_symlink():
return False
if self._is_file is None:
self._is_file = os.path.isfile(self._path)
return self._is_file
def is_symlink(self):
"""Whether this path is a symbolic link."""
if self._is_symlink is None:
self._is_symlink = os.path.islink(self._path)
return self._is_symlink
class ReadableLocalPath(_ReadablePath, LexicalPath):
"""
Simple implementation of a ReadablePath class for local filesystem paths.
"""
__slots__ = ('info',)
def __init__(self, *pathsegments):
super().__init__(*pathsegments)
self.info = LocalPathInfo(self)
def __fspath__(self):
return str(self)
def __open_rb__(self, buffering=-1):
return open(self, 'rb')
def iterdir(self):
return (self / name for name in os.listdir(self))
def readlink(self):
return self.with_segments(os.readlink(self))
class WritableLocalPath(_WritablePath, LexicalPath):
"""
Simple implementation of a WritablePath class for local filesystem paths.
"""
__slots__ = ()
def __fspath__(self):
return str(self)
def __open_wb__(self, buffering=-1):
return open(self, 'wb')
def mkdir(self, mode=0o777):
os.mkdir(self, mode)
def symlink_to(self, target, target_is_directory=False):
os.symlink(target, self, target_is_directory)
+336
View File
@@ -0,0 +1,336 @@
"""
Implementations of ReadablePath and WritablePath for zip file members, for use
in pathlib tests.
ZipPathGround is also defined here. It helps establish the "ground truth"
about zip file members in tests.
"""
import errno
import io
import posixpath
import stat
import zipfile
from stat import S_IFMT, S_ISDIR, S_ISREG, S_ISLNK
from . import is_pypi
if is_pypi:
from pathlib_abc import PathInfo, _ReadablePath, _WritablePath
else:
from pathlib.types import PathInfo, _ReadablePath, _WritablePath
class ZipPathGround:
can_symlink = True
def __init__(self, path_cls):
self.path_cls = path_cls
def setup(self, local_suffix=""):
return self.path_cls(zip_file=zipfile.ZipFile(io.BytesIO(), "w"))
def teardown(self, root):
root.zip_file.close()
def create_file(self, path, data=b''):
path.zip_file.writestr(str(path), data)
def create_dir(self, path):
zip_info = zipfile.ZipInfo(str(path) + '/')
zip_info.external_attr |= stat.S_IFDIR << 16
zip_info.external_attr |= stat.FILE_ATTRIBUTE_DIRECTORY
path.zip_file.writestr(zip_info, '')
def create_symlink(self, path, target):
zip_info = zipfile.ZipInfo(str(path))
zip_info.external_attr = stat.S_IFLNK << 16
path.zip_file.writestr(zip_info, target.encode())
def create_hierarchy(self, p):
# Add regular files
self.create_file(p.joinpath('fileA'), b'this is file A\n')
self.create_file(p.joinpath('dirB/fileB'), b'this is file B\n')
self.create_file(p.joinpath('dirC/fileC'), b'this is file C\n')
self.create_file(p.joinpath('dirC/dirD/fileD'), b'this is file D\n')
self.create_file(p.joinpath('dirC/novel.txt'), b'this is a novel\n')
# Add symlinks
self.create_symlink(p.joinpath('linkA'), 'fileA')
self.create_symlink(p.joinpath('linkB'), 'dirB')
self.create_symlink(p.joinpath('dirA/linkC'), '../dirB')
self.create_symlink(p.joinpath('brokenLink'), 'non-existing')
self.create_symlink(p.joinpath('brokenLinkLoop'), 'brokenLinkLoop')
def readtext(self, p):
with p.zip_file.open(str(p), 'r') as f:
f = io.TextIOWrapper(f, encoding='utf-8')
return f.read()
def readbytes(self, p):
with p.zip_file.open(str(p), 'r') as f:
return f.read()
readlink = readtext
def isdir(self, p):
path_str = str(p) + "/"
return path_str in p.zip_file.NameToInfo
def isfile(self, p):
info = p.zip_file.NameToInfo.get(str(p))
if info is None:
return False
return not stat.S_ISLNK(info.external_attr >> 16)
def islink(self, p):
info = p.zip_file.NameToInfo.get(str(p))
if info is None:
return False
return stat.S_ISLNK(info.external_attr >> 16)
class MissingZipPathInfo(PathInfo):
"""
PathInfo implementation that is used when a zip file member is missing.
"""
__slots__ = ()
def exists(self, follow_symlinks=True):
return False
def is_dir(self, follow_symlinks=True):
return False
def is_file(self, follow_symlinks=True):
return False
def is_symlink(self):
return False
def resolve(self):
return self
missing_zip_path_info = MissingZipPathInfo()
class ZipPathInfo(PathInfo):
"""
PathInfo implementation for an existing zip file member.
"""
__slots__ = ('zip_file', 'zip_info', 'parent', 'children')
def __init__(self, zip_file, parent=None):
self.zip_file = zip_file
self.zip_info = None
self.parent = parent or self
self.children = {}
def exists(self, follow_symlinks=True):
if follow_symlinks and self.is_symlink():
return self.resolve().exists()
return True
def is_dir(self, follow_symlinks=True):
if follow_symlinks and self.is_symlink():
return self.resolve().is_dir()
elif self.zip_info is None:
return True
elif fmt := S_IFMT(self.zip_info.external_attr >> 16):
return S_ISDIR(fmt)
else:
return self.zip_info.filename.endswith('/')
def is_file(self, follow_symlinks=True):
if follow_symlinks and self.is_symlink():
return self.resolve().is_file()
elif self.zip_info is None:
return False
elif fmt := S_IFMT(self.zip_info.external_attr >> 16):
return S_ISREG(fmt)
else:
return not self.zip_info.filename.endswith('/')
def is_symlink(self):
if self.zip_info is None:
return False
elif fmt := S_IFMT(self.zip_info.external_attr >> 16):
return S_ISLNK(fmt)
else:
return False
def resolve(self, path=None, create=False, follow_symlinks=True):
"""
Traverse zip hierarchy (parents, children and symlinks) starting
from this PathInfo. This is called from three places:
- When a zip file member is added to ZipFile.filelist, this method
populates the ZipPathInfo tree (using create=True).
- When ReadableZipPath.info is accessed, this method is finds a
ZipPathInfo entry for the path without resolving any final symlink
(using follow_symlinks=False)
- When ZipPathInfo methods are called with follow_symlinks=True, this
method resolves any symlink in the final path position.
"""
link_count = 0
stack = path.split('/')[::-1] if path else []
info = self
while True:
if info.is_symlink() and (follow_symlinks or stack):
link_count += 1
if link_count >= 40:
return missing_zip_path_info # Symlink loop!
path = info.zip_file.read(info.zip_info).decode()
stack += path.split('/')[::-1] if path else []
info = info.parent
if stack:
name = stack.pop()
else:
return info
if name == '..':
info = info.parent
elif name and name != '.':
if name not in info.children:
if create:
info.children[name] = ZipPathInfo(info.zip_file, info)
else:
return missing_zip_path_info # No such child!
info = info.children[name]
class ZipFileList:
"""
`list`-like object that we inject as `ZipFile.filelist`. We maintain a
tree of `ZipPathInfo` objects representing the zip file members.
"""
__slots__ = ('tree', '_items')
def __init__(self, zip_file):
self.tree = ZipPathInfo(zip_file)
self._items = []
for item in zip_file.filelist:
self.append(item)
def __len__(self):
return len(self._items)
def __iter__(self):
return iter(self._items)
def append(self, item):
self._items.append(item)
self.tree.resolve(item.filename, create=True).zip_info = item
class ReadableZipPath(_ReadablePath):
"""
Simple implementation of a ReadablePath class for .zip files.
"""
__slots__ = ('_segments', 'zip_file')
parser = posixpath
def __init__(self, *pathsegments, zip_file):
self._segments = pathsegments
self.zip_file = zip_file
if not isinstance(zip_file.filelist, ZipFileList):
zip_file.filelist = ZipFileList(zip_file)
def __hash__(self):
return hash((str(self), self.zip_file))
def __eq__(self, other):
if not isinstance(other, ReadableZipPath):
return NotImplemented
return str(self) == str(other) and self.zip_file is other.zip_file
def __str__(self):
if not self._segments:
return ''
return self.parser.join(*self._segments)
def __repr__(self):
return f'{type(self).__name__}({str(self)!r}, zip_file={self.zip_file!r})'
def with_segments(self, *pathsegments):
return type(self)(*pathsegments, zip_file=self.zip_file)
@property
def info(self):
tree = self.zip_file.filelist.tree
return tree.resolve(str(self), follow_symlinks=False)
def __open_rb__(self, buffering=-1):
info = self.info.resolve()
if not info.exists():
raise FileNotFoundError(errno.ENOENT, "File not found", self)
elif info.is_dir():
raise IsADirectoryError(errno.EISDIR, "Is a directory", self)
return self.zip_file.open(info.zip_info, 'r')
def iterdir(self):
info = self.info.resolve()
if not info.exists():
raise FileNotFoundError(errno.ENOENT, "File not found", self)
elif not info.is_dir():
raise NotADirectoryError(errno.ENOTDIR, "Not a directory", self)
return (self / name for name in info.children)
def readlink(self):
info = self.info
if not info.exists():
raise FileNotFoundError(errno.ENOENT, "File not found", self)
elif not info.is_symlink():
raise OSError(errno.EINVAL, "Not a symlink", self)
return self.with_segments(self.zip_file.read(info.zip_info).decode())
class WritableZipPath(_WritablePath):
"""
Simple implementation of a WritablePath class for .zip files.
"""
__slots__ = ('_segments', 'zip_file')
parser = posixpath
def __init__(self, *pathsegments, zip_file):
self._segments = pathsegments
self.zip_file = zip_file
def __hash__(self):
return hash((str(self), self.zip_file))
def __eq__(self, other):
if not isinstance(other, WritableZipPath):
return NotImplemented
return str(self) == str(other) and self.zip_file is other.zip_file
def __str__(self):
if not self._segments:
return ''
return self.parser.join(*self._segments)
def __repr__(self):
return f'{type(self).__name__}({str(self)!r}, zip_file={self.zip_file!r})'
def with_segments(self, *pathsegments):
return type(self)(*pathsegments, zip_file=self.zip_file)
def __open_wb__(self, buffering=-1):
return self.zip_file.open(str(self), 'w')
def mkdir(self, mode=0o777):
zinfo = zipfile.ZipInfo(str(self) + '/')
zinfo.external_attr |= stat.S_IFDIR << 16
zinfo.external_attr |= stat.FILE_ATTRIBUTE_DIRECTORY
self.zip_file.writestr(zinfo, '')
def symlink_to(self, target, target_is_directory=False):
zinfo = zipfile.ZipInfo(str(self))
zinfo.external_attr = stat.S_IFLNK << 16
if target_is_directory:
zinfo.external_attr |= 0x10
self.zip_file.writestr(zinfo, str(target))
+174
View File
@@ -0,0 +1,174 @@
"""
Tests for copying from pathlib.types._ReadablePath to _WritablePath.
"""
import contextlib
import unittest
from .support import is_pypi
from .support.local_path import LocalPathGround
from .support.zip_path import ZipPathGround, ReadableZipPath, WritableZipPath
class CopyTestBase:
def setUp(self):
self.source_root = self.source_ground.setup()
self.source_ground.create_hierarchy(self.source_root)
self.target_root = self.target_ground.setup(local_suffix="_target")
def tearDown(self):
self.source_ground.teardown(self.source_root)
self.target_ground.teardown(self.target_root)
def test_copy_file(self):
source = self.source_root / 'fileA'
target = self.target_root / 'copyA'
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isfile(target))
self.assertEqual(self.source_ground.readbytes(source),
self.target_ground.readbytes(result))
def test_copy_file_empty(self):
source = self.source_root / 'empty'
target = self.target_root / 'copyA'
self.source_ground.create_file(source, b'')
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isfile(target))
self.assertEqual(self.target_ground.readbytes(result), b'')
def test_copy_file_to_existing_file(self):
source = self.source_root / 'fileA'
target = self.target_root / 'copyA'
self.target_ground.create_file(target, b'this is a copy\n')
with contextlib.ExitStack() as stack:
if isinstance(target, WritableZipPath):
stack.enter_context(self.assertWarns(UserWarning))
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isfile(target))
self.assertEqual(self.source_ground.readbytes(source),
self.target_ground.readbytes(result))
def test_copy_file_to_directory(self):
if isinstance(self.target_root, WritableZipPath):
self.skipTest('needs local target')
source = self.source_root / 'fileA'
target = self.target_root / 'copyA'
self.target_ground.create_dir(target)
self.assertRaises(OSError, source.copy, target)
def test_copy_file_to_itself(self):
source = self.source_root / 'fileA'
self.assertRaises(OSError, source.copy, source)
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)
def test_copy_dir(self):
source = self.source_root / 'dirC'
target = self.target_root / 'copyC'
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isdir(target))
self.assertTrue(self.target_ground.isfile(target / 'fileC'))
self.assertEqual(self.target_ground.readtext(target / 'fileC'), 'this is file C\n')
self.assertTrue(self.target_ground.isdir(target / 'dirD'))
self.assertTrue(self.target_ground.isfile(target / 'dirD' / 'fileD'))
self.assertEqual(self.target_ground.readtext(target / 'dirD' / 'fileD'), 'this is file D\n')
def test_copy_dir_follow_symlinks_true(self):
if not self.source_ground.can_symlink:
self.skipTest('needs symlink support on source')
source = self.source_root / 'dirC'
target = self.target_root / 'copyC'
self.source_ground.create_symlink(source / 'linkC', 'fileC')
self.source_ground.create_symlink(source / 'linkD', 'dirD')
result = source.copy(target)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isdir(target))
self.assertFalse(self.target_ground.islink(target / 'linkC'))
self.assertTrue(self.target_ground.isfile(target / 'linkC'))
self.assertEqual(self.target_ground.readtext(target / 'linkC'), 'this is file C\n')
self.assertFalse(self.target_ground.islink(target / 'linkD'))
self.assertTrue(self.target_ground.isdir(target / 'linkD'))
self.assertTrue(self.target_ground.isfile(target / 'linkD' / 'fileD'))
self.assertEqual(self.target_ground.readtext(target / 'linkD' / 'fileD'), 'this is file D\n')
def test_copy_dir_follow_symlinks_false(self):
if not self.source_ground.can_symlink:
self.skipTest('needs symlink support on source')
if not self.target_ground.can_symlink:
self.skipTest('needs symlink support on target')
source = self.source_root / 'dirC'
target = self.target_root / 'copyC'
self.source_ground.create_symlink(source / 'linkC', 'fileC')
self.source_ground.create_symlink(source / 'linkD', 'dirD')
result = source.copy(target, follow_symlinks=False)
self.assertEqual(result, target)
self.assertTrue(self.target_ground.isdir(target))
self.assertTrue(self.target_ground.islink(target / 'linkC'))
self.assertEqual(self.target_ground.readlink(target / 'linkC'), 'fileC')
self.assertTrue(self.target_ground.islink(target / 'linkD'))
self.assertEqual(self.target_ground.readlink(target / 'linkD'), 'dirD')
def test_copy_dir_to_existing_directory(self):
if isinstance(self.target_root, WritableZipPath):
self.skipTest('needs local target')
source = self.source_root / 'dirC'
target = self.target_root / 'copyC'
self.target_ground.create_dir(target)
self.assertRaises(FileExistsError, source.copy, target)
def test_copy_dir_to_itself(self):
source = self.source_root / 'dirC'
self.assertRaises(OSError, source.copy, source)
self.assertRaises(OSError, source.copy, source, follow_symlinks=False)
def test_copy_dir_into_itself(self):
source = self.source_root / 'dirC'
target = self.source_root / 'dirC' / 'dirD' / 'copyC'
self.assertRaises(OSError, source.copy, target)
self.assertRaises(OSError, source.copy, target, follow_symlinks=False)
def test_copy_into(self):
source = self.source_root / 'fileA'
target_dir = self.target_root / 'dirA'
self.target_ground.create_dir(target_dir)
result = source.copy_into(target_dir)
self.assertEqual(result, target_dir / 'fileA')
self.assertTrue(self.target_ground.isfile(result))
self.assertEqual(self.source_ground.readbytes(source),
self.target_ground.readbytes(result))
def test_copy_into_empty_name(self):
source = self.source_root.with_segments()
target_dir = self.target_root / 'dirA'
self.target_ground.create_dir(target_dir)
self.assertRaises(ValueError, source.copy_into, target_dir)
class ZipToZipPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = ZipPathGround(ReadableZipPath)
target_ground = ZipPathGround(WritableZipPath)
if not is_pypi:
from pathlib import Path
class ZipToLocalPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = ZipPathGround(ReadableZipPath)
target_ground = LocalPathGround(Path)
class LocalToZipPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = LocalPathGround(Path)
target_ground = ZipPathGround(WritableZipPath)
class LocalToLocalPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = LocalPathGround(Path)
target_ground = LocalPathGround(Path)
if __name__ == "__main__":
unittest.main()
+395
View File
@@ -0,0 +1,395 @@
"""
Tests for pathlib.types._JoinablePath
"""
import unittest
import threading
from test.support import threading_helper
from .support import is_pypi
from .support.lexical_path import LexicalPath
if is_pypi:
from pathlib_abc import _PathParser, _JoinablePath
else:
from pathlib.types import _PathParser, _JoinablePath
class JoinTestBase:
def test_is_joinable(self):
p = self.cls()
self.assertIsInstance(p, _JoinablePath)
def test_parser(self):
self.assertIsInstance(self.cls.parser, _PathParser)
def test_constructor(self):
P = self.cls
p = P('a')
self.assertIsInstance(p, P)
P()
P('a', 'b', 'c')
P('/a', 'b', 'c')
P('a/b/c')
P('/a/b/c')
def test_with_segments(self):
class P(self.cls):
def __init__(self, *pathsegments, session_id):
super().__init__(*pathsegments)
self.session_id = session_id
def with_segments(self, *pathsegments):
return type(self)(*pathsegments, session_id=self.session_id)
p = P('foo', 'bar', session_id=42)
self.assertEqual(42, (p / 'foo').session_id)
self.assertEqual(42, ('foo' / p).session_id)
self.assertEqual(42, p.joinpath('foo').session_id)
self.assertEqual(42, p.with_name('foo').session_id)
self.assertEqual(42, p.with_stem('foo').session_id)
self.assertEqual(42, p.with_suffix('.foo').session_id)
self.assertEqual(42, p.with_segments('foo').session_id)
self.assertEqual(42, p.parent.session_id)
for parent in p.parents:
self.assertEqual(42, parent.session_id)
def test_join(self):
P = self.cls
sep = self.cls.parser.sep
p = P(f'a{sep}b')
pp = p.joinpath('c')
self.assertEqual(pp, P(f'a{sep}b{sep}c'))
self.assertIs(type(pp), type(p))
pp = p.joinpath('c', 'd')
self.assertEqual(pp, P(f'a{sep}b{sep}c{sep}d'))
pp = p.joinpath(f'{sep}c')
self.assertEqual(pp, P(f'{sep}c'))
def test_div(self):
# Basically the same as joinpath().
P = self.cls
sep = self.cls.parser.sep
p = P(f'a{sep}b')
pp = p / 'c'
self.assertEqual(pp, P(f'a{sep}b{sep}c'))
self.assertIs(type(pp), type(p))
pp = p / f'c{sep}d'
self.assertEqual(pp, P(f'a{sep}b{sep}c{sep}d'))
pp = p / 'c' / 'd'
self.assertEqual(pp, P(f'a{sep}b{sep}c{sep}d'))
pp = 'c' / p / 'd'
self.assertEqual(pp, P(f'c{sep}a{sep}b{sep}d'))
pp = p/ f'{sep}c'
self.assertEqual(pp, P(f'{sep}c'))
def test_full_match(self):
P = self.cls
# Simple relative pattern.
self.assertTrue(P('b.py').full_match('b.py'))
self.assertFalse(P('a/b.py').full_match('b.py'))
self.assertFalse(P('/a/b.py').full_match('b.py'))
self.assertFalse(P('a.py').full_match('b.py'))
self.assertFalse(P('b/py').full_match('b.py'))
self.assertFalse(P('/a.py').full_match('b.py'))
self.assertFalse(P('b.py/c').full_match('b.py'))
# Wildcard relative pattern.
self.assertTrue(P('b.py').full_match('*.py'))
self.assertFalse(P('a/b.py').full_match('*.py'))
self.assertFalse(P('/a/b.py').full_match('*.py'))
self.assertFalse(P('b.pyc').full_match('*.py'))
self.assertFalse(P('b./py').full_match('*.py'))
self.assertFalse(P('b.py/c').full_match('*.py'))
# Multi-part relative pattern.
self.assertTrue(P('ab/c.py').full_match('a*/*.py'))
self.assertFalse(P('/d/ab/c.py').full_match('a*/*.py'))
self.assertFalse(P('a.py').full_match('a*/*.py'))
self.assertFalse(P('/dab/c.py').full_match('a*/*.py'))
self.assertFalse(P('ab/c.py/d').full_match('a*/*.py'))
# Absolute pattern.
self.assertTrue(P('/b.py').full_match('/*.py'))
self.assertFalse(P('b.py').full_match('/*.py'))
self.assertFalse(P('a/b.py').full_match('/*.py'))
self.assertFalse(P('/a/b.py').full_match('/*.py'))
# Multi-part absolute pattern.
self.assertTrue(P('/a/b.py').full_match('/a/*.py'))
self.assertFalse(P('/ab.py').full_match('/a/*.py'))
self.assertFalse(P('/a/b/c.py').full_match('/a/*.py'))
# Multi-part glob-style pattern.
self.assertTrue(P('a').full_match('**'))
self.assertTrue(P('c.py').full_match('**'))
self.assertTrue(P('a/b/c.py').full_match('**'))
self.assertTrue(P('/a/b/c.py').full_match('**'))
self.assertTrue(P('/a/b/c.py').full_match('/**'))
self.assertTrue(P('/a/b/c.py').full_match('/a/**'))
self.assertTrue(P('/a/b/c.py').full_match('**/*.py'))
self.assertTrue(P('/a/b/c.py').full_match('/**/*.py'))
self.assertTrue(P('/a/b/c.py').full_match('/a/**/*.py'))
self.assertTrue(P('/a/b/c.py').full_match('/a/b/**/*.py'))
self.assertTrue(P('/a/b/c.py').full_match('/**/**/**/**/*.py'))
self.assertFalse(P('c.py').full_match('**/a.py'))
self.assertFalse(P('c.py').full_match('c/**'))
self.assertFalse(P('a/b/c.py').full_match('**/a'))
self.assertFalse(P('a/b/c.py').full_match('**/a/b'))
self.assertFalse(P('a/b/c.py').full_match('**/a/b/c'))
self.assertFalse(P('a/b/c.py').full_match('**/a/b/c.'))
self.assertFalse(P('a/b/c.py').full_match('**/a/b/c./**'))
self.assertFalse(P('a/b/c.py').full_match('**/a/b/c./**'))
self.assertFalse(P('a/b/c.py').full_match('/a/b/c.py/**'))
self.assertFalse(P('a/b/c.py').full_match('/**/a/b/c.py'))
# Matching against empty path
self.assertFalse(P('').full_match('*'))
self.assertTrue(P('').full_match('**'))
self.assertFalse(P('').full_match('**/*'))
# Matching with empty pattern
self.assertTrue(P('').full_match(''))
self.assertTrue(P('.').full_match('.'))
self.assertFalse(P('/').full_match(''))
self.assertFalse(P('/').full_match('.'))
self.assertFalse(P('foo').full_match(''))
self.assertFalse(P('foo').full_match('.'))
def test_parts(self):
# `parts` returns a tuple.
sep = self.cls.parser.sep
P = self.cls
p = P(f'a{sep}b')
parts = p.parts
self.assertEqual(parts, ('a', 'b'))
# When the path is absolute, the anchor is a separate part.
p = P(f'{sep}a{sep}b')
parts = p.parts
self.assertEqual(parts, (sep, 'a', 'b'))
@threading_helper.requires_working_threading()
def test_parts_multithreaded(self):
P = self.cls
NUM_THREADS = 10
NUM_ITERS = 10
for _ in range(NUM_ITERS):
b = threading.Barrier(NUM_THREADS)
path = P('a') / 'b' / 'c' / 'd' / 'e'
expected = ('a', 'b', 'c', 'd', 'e')
def check_parts():
b.wait()
self.assertEqual(path.parts, expected)
threads = [threading.Thread(target=check_parts) for _ in range(NUM_THREADS)]
with threading_helper.start_threads(threads):
pass
def test_parent(self):
# Relative
P = self.cls
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(''))
# Anchored
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(self):
# Relative
P = self.cls
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]
with self.assertRaises(TypeError):
par[0] = p
# Anchored
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_anchor(self):
P = self.cls
sep = self.cls.parser.sep
self.assertEqual(P('').anchor, '')
self.assertEqual(P(f'a{sep}b').anchor, '')
self.assertEqual(P(sep).anchor, sep)
self.assertEqual(P(f'{sep}a{sep}b').anchor, sep)
def test_name(self):
P = self.cls
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.py').name, 'b.py')
self.assertEqual(P('/a/b.py').name, 'b.py')
def test_suffix(self):
P = self.cls
self.assertEqual(P('').suffix, '')
self.assertEqual(P('.').suffix, '')
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.py').suffix, '.py')
self.assertEqual(P('/a/b.py').suffix, '.py')
self.assertEqual(P('a/.hgrc').suffix, '')
self.assertEqual(P('/a/.hgrc').suffix, '')
self.assertEqual(P('a/.hg.rc').suffix, '.rc')
self.assertEqual(P('/a/.hg.rc').suffix, '.rc')
self.assertEqual(P('a/b.tar.gz').suffix, '.gz')
self.assertEqual(P('/a/b.tar.gz').suffix, '.gz')
self.assertEqual(P('a/trailing.dot.').suffix, '.')
self.assertEqual(P('/a/trailing.dot.').suffix, '.')
self.assertEqual(P('a/..d.o.t..').suffix, '.')
self.assertEqual(P('a/inn.er..dots').suffix, '.dots')
self.assertEqual(P('photo').suffix, '')
self.assertEqual(P('photo.jpg').suffix, '.jpg')
def test_suffixes(self):
P = self.cls
self.assertEqual(P('').suffixes, [])
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.py').suffixes, ['.py'])
self.assertEqual(P('/a/b.py').suffixes, ['.py'])
self.assertEqual(P('a/.hgrc').suffixes, [])
self.assertEqual(P('/a/.hgrc').suffixes, [])
self.assertEqual(P('a/.hg.rc').suffixes, ['.rc'])
self.assertEqual(P('/a/.hg.rc').suffixes, ['.rc'])
self.assertEqual(P('a/b.tar.gz').suffixes, ['.tar', '.gz'])
self.assertEqual(P('/a/b.tar.gz').suffixes, ['.tar', '.gz'])
self.assertEqual(P('a/trailing.dot.').suffixes, ['.dot', '.'])
self.assertEqual(P('/a/trailing.dot.').suffixes, ['.dot', '.'])
self.assertEqual(P('a/..d.o.t..').suffixes, ['.o', '.t', '.', '.'])
self.assertEqual(P('a/inn.er..dots').suffixes, ['.er', '.', '.dots'])
self.assertEqual(P('photo').suffixes, [])
self.assertEqual(P('photo.jpg').suffixes, ['.jpg'])
def test_stem(self):
P = self.cls
self.assertEqual(P('..').stem, '..')
self.assertEqual(P('').stem, '')
self.assertEqual(P('/').stem, '')
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/trailing.dot.').stem, 'trailing.dot')
self.assertEqual(P('a/..d.o.t..').stem, '..d.o.t.')
self.assertEqual(P('a/inn.er..dots').stem, 'inn.er.')
self.assertEqual(P('photo').stem, 'photo')
self.assertEqual(P('photo.jpg').stem, 'photo')
def test_with_name(self):
P = self.cls
self.assertEqual(P('a/b').with_name('d.xml'), P('a/d.xml'))
self.assertEqual(P('/a/b').with_name('d.xml'), P('/a/d.xml'))
self.assertEqual(P('a/b.py').with_name('d.xml'), P('a/d.xml'))
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.assertRaises(ValueError, P('a/b').with_name, '/c')
self.assertRaises(ValueError, P('a/b').with_name, 'c/')
self.assertRaises(ValueError, P('a/b').with_name, 'c/d')
def test_with_stem(self):
P = self.cls
self.assertEqual(P('a/b').with_stem('d'), P('a/d'))
self.assertEqual(P('/a/b').with_stem('d'), P('/a/d'))
self.assertEqual(P('a/b.py').with_stem('d'), P('a/d.py'))
self.assertEqual(P('/a/b.py').with_stem('d'), P('/a/d.py'))
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.assertRaises(ValueError, P('foo.gz').with_stem, '')
self.assertRaises(ValueError, P('/a/b/foo.gz').with_stem, '')
self.assertRaises(ValueError, P('a/b').with_stem, '/c')
self.assertRaises(ValueError, P('a/b').with_stem, 'c/')
self.assertRaises(ValueError, P('a/b').with_stem, 'c/d')
def test_with_suffix(self):
P = self.cls
self.assertEqual(P('a/b').with_suffix('.gz'), P('a/b.gz'))
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'))
# Stripping suffix.
self.assertEqual(P('a/b.py').with_suffix(''), P('a/b'))
self.assertEqual(P('/a/b').with_suffix(''), P('/a/b'))
# Single dot
self.assertEqual(P('a/b').with_suffix('.'), P('a/b.'))
self.assertEqual(P('/a/b').with_suffix('.'), P('/a/b.'))
self.assertEqual(P('a/b.py').with_suffix('.'), P('a/b.'))
self.assertEqual(P('/a/b.py').with_suffix('.'), P('/a/b.'))
# Path doesn't have a "filename" component.
self.assertRaises(ValueError, P('').with_suffix, '.gz')
self.assertRaises(ValueError, P('/').with_suffix, '.gz')
# Invalid suffix.
self.assertRaises(ValueError, P('a/b').with_suffix, 'gz')
self.assertRaises(ValueError, P('a/b').with_suffix, '/')
self.assertRaises(ValueError, P('a/b').with_suffix, '/.gz')
self.assertRaises(ValueError, P('a/b').with_suffix, 'c/d')
self.assertRaises(ValueError, P('a/b').with_suffix, '.c/.d')
self.assertRaises(ValueError, P('a/b').with_suffix, './.d')
self.assertRaises(ValueError, P('a/b').with_suffix, '.d/.')
self.assertRaises(TypeError, P('a/b').with_suffix, None)
class LexicalPathJoinTest(JoinTestBase, unittest.TestCase):
cls = LexicalPath
if not is_pypi:
from pathlib import PurePath, Path
class PurePathJoinTest(JoinTestBase, unittest.TestCase):
cls = PurePath
class PathJoinTest(JoinTestBase, unittest.TestCase):
cls = Path
if __name__ == "__main__":
unittest.main()
+51
View File
@@ -0,0 +1,51 @@
"""
Tests for Posix-flavoured pathlib.types._JoinablePath
"""
import os
import unittest
from .support import is_pypi
from .support.lexical_path import LexicalPosixPath
class JoinTestBase:
def test_join(self):
P = self.cls
p = P('//a')
pp = p.joinpath('b')
self.assertEqual(pp, P('//a/b'))
pp = P('/a').joinpath('//c')
self.assertEqual(pp, P('//c'))
pp = P('//a').joinpath('/c')
self.assertEqual(pp, P('/c'))
def test_div(self):
# Basically the same as joinpath().
P = self.cls
p = P('//a')
pp = p / 'b'
self.assertEqual(pp, P('//a/b'))
pp = P('/a') / '//c'
self.assertEqual(pp, P('//c'))
pp = P('//a') / '/c'
self.assertEqual(pp, P('/c'))
class LexicalPosixPathJoinTest(JoinTestBase, unittest.TestCase):
cls = LexicalPosixPath
if not is_pypi:
from pathlib import PurePosixPath, PosixPath
class PurePosixPathJoinTest(JoinTestBase, unittest.TestCase):
cls = PurePosixPath
if os.name != 'nt':
class PosixPathJoinTest(JoinTestBase, unittest.TestCase):
cls = PosixPath
if __name__ == "__main__":
unittest.main()
+290
View File
@@ -0,0 +1,290 @@
"""
Tests for Windows-flavoured pathlib.types._JoinablePath
"""
import os
import unittest
from .support import is_pypi
from .support.lexical_path import LexicalWindowsPath
class JoinTestBase:
def test_join(self):
P = self.cls
p = P('C:/a/b')
pp = p.joinpath('x/y')
self.assertEqual(pp, P(r'C:/a/b\x/y'))
pp = p.joinpath('/x/y')
self.assertEqual(pp, P('C:/x/y'))
# Joining with a different drive => the first path is ignored, even
# if the second path is relative.
pp = p.joinpath('D:x/y')
self.assertEqual(pp, P('D:x/y'))
pp = p.joinpath('D:/x/y')
self.assertEqual(pp, P('D:/x/y'))
pp = p.joinpath('//host/share/x/y')
self.assertEqual(pp, P('//host/share/x/y'))
# Joining with the same drive => the first path is appended to if
# the second path is relative.
pp = p.joinpath('c:x/y')
self.assertEqual(pp, P(r'c:/a/b\x/y'))
pp = p.joinpath('c:/x/y')
self.assertEqual(pp, P('c:/x/y'))
# Joining with files with NTFS data streams => the filename should
# not be parsed as a drive letter
pp = p.joinpath('./d:s')
self.assertEqual(pp, P(r'C:/a/b\./d:s'))
pp = p.joinpath('./dd:s')
self.assertEqual(pp, P(r'C:/a/b\./dd:s'))
pp = p.joinpath('E:d:s')
self.assertEqual(pp, P('E:d:s'))
# Joining onto a UNC path with no root
pp = P('//server').joinpath('share')
self.assertEqual(pp, P(r'//server\share'))
pp = P('//./BootPartition').joinpath('Windows')
self.assertEqual(pp, P(r'//./BootPartition\Windows'))
def test_div(self):
# Basically the same as joinpath().
P = self.cls
p = P('C:/a/b')
self.assertEqual(p / 'x/y', P(r'C:/a/b\x/y'))
self.assertEqual(p / 'x' / 'y', P(r'C:/a/b\x\y'))
self.assertEqual(p / '/x/y', P('C:/x/y'))
self.assertEqual(p / '/x' / 'y', P(r'C:/x\y'))
# Joining with a different drive => the first path is ignored, even
# if the second path is relative.
self.assertEqual(p / 'D:x/y', P('D:x/y'))
self.assertEqual(p / 'D:' / 'x/y', P('D:x/y'))
self.assertEqual(p / 'D:/x/y', P('D:/x/y'))
self.assertEqual(p / 'D:' / '/x/y', P('D:/x/y'))
self.assertEqual(p / '//host/share/x/y', P('//host/share/x/y'))
# Joining with the same drive => the first path is appended to if
# the second path is relative.
self.assertEqual(p / 'c:x/y', P(r'c:/a/b\x/y'))
self.assertEqual(p / 'c:/x/y', P('c:/x/y'))
# Joining with files with NTFS data streams => the filename should
# not be parsed as a drive letter
self.assertEqual(p / './d:s', P(r'C:/a/b\./d:s'))
self.assertEqual(p / './dd:s', P(r'C:/a/b\./dd:s'))
self.assertEqual(p / 'E:d:s', P('E:d:s'))
def test_str(self):
p = self.cls(r'a\b\c')
self.assertEqual(str(p), 'a\\b\\c')
p = self.cls(r'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(r'\\a\b\c')
self.assertEqual(str(p), '\\\\a\\b\\c')
p = self.cls(r'\\a\b\c\d')
self.assertEqual(str(p), '\\\\a\\b\\c\\d')
def test_parts(self):
P = self.cls
p = P(r'c:a\b')
parts = p.parts
self.assertEqual(parts, ('c:', 'a', 'b'))
p = P(r'c:\a\b')
parts = p.parts
self.assertEqual(parts, ('c:\\', 'a', 'b'))
p = P(r'\\a\b\c\d')
parts = p.parts
self.assertEqual(parts, ('\\\\a\\b\\', 'c', 'd'))
def test_parent(self):
# Anchored
P = self.cls
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:'))
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:/'))
p = P('//a/b/c/d')
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/'))
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)
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('//a/b/c/d')
par = p.parents
self.assertEqual(len(par), 2)
self.assertEqual(par[0], P('//a/b/c'))
self.assertEqual(par[1], P('//a/b/'))
self.assertEqual(par[0:1], (P('//a/b/c'),))
self.assertEqual(par[0:-1], (P('//a/b/c'),))
self.assertEqual(par[:2], (P('//a/b/c'), P('//a/b/')))
self.assertEqual(par[1:], (P('//a/b/'),))
self.assertEqual(par[::2], (P('//a/b/c'),))
self.assertEqual(par[::-1], (P('//a/b/'), P('//a/b/c')))
self.assertEqual(list(par), [P('//a/b/c'), P('//a/b/')])
with self.assertRaises(IndexError):
par[2]
def test_anchor(self):
P = self.cls
self.assertEqual(P('c:').anchor, 'c:')
self.assertEqual(P('c:a/b').anchor, 'c:')
self.assertEqual(P('c:\\').anchor, 'c:\\')
self.assertEqual(P('c:\\a\\b\\').anchor, 'c:\\')
self.assertEqual(P('\\\\a\\b\\').anchor, '\\\\a\\b\\')
self.assertEqual(P('\\\\a\\b\\c\\d').anchor, '\\\\a\\b\\')
def test_name(self):
P = self.cls
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.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')
def test_stem(self):
P = self.cls
self.assertEqual(P('c:').stem, '')
self.assertEqual(P('c:..').stem, '..')
self.assertEqual(P('c:/').stem, '')
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/trailing.dot.').stem, 'trailing.dot')
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.py').suffix, '.py')
self.assertEqual(P('c:/a/b.py').suffix, '.py')
self.assertEqual(P('c:a/.hgrc').suffix, '')
self.assertEqual(P('c:/a/.hgrc').suffix, '')
self.assertEqual(P('c:a/.hg.rc').suffix, '.rc')
self.assertEqual(P('c:/a/.hg.rc').suffix, '.rc')
self.assertEqual(P('c:a/b.tar.gz').suffix, '.gz')
self.assertEqual(P('c:/a/b.tar.gz').suffix, '.gz')
self.assertEqual(P('c:a/trailing.dot.').suffix, '.')
self.assertEqual(P('c:/a/trailing.dot.').suffix, '.')
self.assertEqual(P('//My.py/Share.php').suffix, '')
self.assertEqual(P('//My.py/Share.php/a/b').suffix, '')
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.py').suffixes, ['.py'])
self.assertEqual(P('c:/a/b.py').suffixes, ['.py'])
self.assertEqual(P('c:a/.hgrc').suffixes, [])
self.assertEqual(P('c:/a/.hgrc').suffixes, [])
self.assertEqual(P('c:a/.hg.rc').suffixes, ['.rc'])
self.assertEqual(P('c:/a/.hg.rc').suffixes, ['.rc'])
self.assertEqual(P('c:a/b.tar.gz').suffixes, ['.tar', '.gz'])
self.assertEqual(P('c:/a/b.tar.gz').suffixes, ['.tar', '.gz'])
self.assertEqual(P('//My.py/Share.php').suffixes, [])
self.assertEqual(P('//My.py/Share.php/a/b').suffixes, [])
self.assertEqual(P('c:a/trailing.dot.').suffixes, ['.dot', '.'])
self.assertEqual(P('c:/a/trailing.dot.').suffixes, ['.dot', '.'])
def test_with_name(self):
P = self.cls
self.assertEqual(P(r'c:a\b').with_name('d.xml'), P(r'c:a\d.xml'))
self.assertEqual(P(r'c:\a\b').with_name('d.xml'), P(r'c:\a\d.xml'))
self.assertEqual(P(r'c:a\Dot ending.').with_name('d.xml'), P(r'c:a\d.xml'))
self.assertEqual(P(r'c:\a\Dot ending.').with_name('d.xml'), P(r'c:\a\d.xml'))
self.assertRaises(ValueError, P(r'c:a\b').with_name, r'd:\e')
self.assertRaises(ValueError, P(r'c:a\b').with_name, r'\\My\Share')
def test_with_stem(self):
P = self.cls
self.assertEqual(P('c:a/b').with_stem('d'), P('c:a/d'))
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.assertRaises(ValueError, P('c:a/b').with_stem, 'd:/e')
self.assertRaises(ValueError, P('c:a/b').with_stem, '//My/Share')
def test_with_suffix(self):
P = self.cls
self.assertEqual(P('c:a/b').with_suffix('.gz'), P('c:a/b.gz'))
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'))
# Path doesn't have a "filename" component.
self.assertRaises(ValueError, P('').with_suffix, '.gz')
self.assertRaises(ValueError, P('/').with_suffix, '.gz')
self.assertRaises(ValueError, P('//My/Share').with_suffix, '.gz')
# Invalid suffix.
self.assertRaises(ValueError, P('c:a/b').with_suffix, 'gz')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '/')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\')
self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '/.gz')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '\\.gz')
self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c:.gz')
self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c/d')
self.assertRaises(ValueError, P('c:a/b').with_suffix, 'c\\d')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c/d')
self.assertRaises(ValueError, P('c:a/b').with_suffix, '.c\\d')
self.assertRaises(TypeError, P('c:a/b').with_suffix, None)
class LexicalWindowsPathJoinTest(JoinTestBase, unittest.TestCase):
cls = LexicalWindowsPath
if not is_pypi:
from pathlib import PureWindowsPath, WindowsPath
class PureWindowsPathJoinTest(JoinTestBase, unittest.TestCase):
cls = PureWindowsPath
if os.name == 'nt':
class WindowsPathJoinTest(JoinTestBase, unittest.TestCase):
cls = WindowsPath
if __name__ == "__main__":
unittest.main()
+2169 -142
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+343
View File
@@ -0,0 +1,343 @@
"""
Tests for pathlib.types._ReadablePath
"""
import collections.abc
import io
import sys
import unittest
from .support import is_pypi
from .support.local_path import ReadableLocalPath, LocalPathGround
from .support.zip_path import ReadableZipPath, ZipPathGround
if is_pypi:
from pathlib_abc import PathInfo, _ReadablePath
from pathlib_abc._os import magic_open
else:
from pathlib.types import PathInfo, _ReadablePath
from pathlib._os import magic_open
class ReadTestBase:
def setUp(self):
self.root = self.ground.setup()
self.ground.create_hierarchy(self.root)
def tearDown(self):
self.ground.teardown(self.root)
def test_is_readable(self):
self.assertIsInstance(self.root, _ReadablePath)
def test_open_r(self):
p = self.root / 'fileA'
with magic_open(p, 'r', encoding='utf-8') as f:
self.assertIsInstance(f, io.TextIOBase)
self.assertEqual(f.read(), 'this is file A\n')
@unittest.skipIf(
not getattr(sys.flags, 'warn_default_encoding', 0),
"Requires warn_default_encoding",
)
def test_open_r_encoding_warning(self):
p = self.root / 'fileA'
with self.assertWarns(EncodingWarning) as wc:
with magic_open(p, 'r'):
pass
self.assertEqual(wc.filename, __file__)
def test_open_rb(self):
p = self.root / 'fileA'
with magic_open(p, 'rb') as f:
self.assertEqual(f.read(), b'this is file A\n')
self.assertRaises(ValueError, magic_open, p, 'rb', encoding='utf8')
self.assertRaises(ValueError, magic_open, p, 'rb', errors='strict')
self.assertRaises(ValueError, magic_open, p, 'rb', newline='')
def test_read_bytes(self):
p = self.root / 'fileA'
self.assertEqual(p.read_bytes(), b'this is file A\n')
def test_read_text(self):
p = self.root / 'fileA'
self.assertEqual(p.read_text(encoding='utf-8'), 'this is file A\n')
q = self.root / 'abc'
self.ground.create_file(q, b'\xe4bcdefg')
self.assertEqual(q.read_text(encoding='latin-1'), 'äbcdefg')
self.assertEqual(q.read_text(encoding='utf-8', errors='ignore'), 'bcdefg')
@unittest.skipIf(
not getattr(sys.flags, 'warn_default_encoding', 0),
"Requires warn_default_encoding",
)
def test_read_text_encoding_warning(self):
p = self.root / 'fileA'
with self.assertWarns(EncodingWarning) as wc:
p.read_text()
self.assertEqual(wc.filename, __file__)
def test_read_text_with_newlines(self):
p = self.root / 'abc'
self.ground.create_file(p, b'abcde\r\nfghlk\n\rmnopq')
# Check that `\n` character change nothing
self.assertEqual(p.read_text(encoding='utf-8', newline='\n'), 'abcde\r\nfghlk\n\rmnopq')
# Check that `\r` character replaces `\n`
self.assertEqual(p.read_text(encoding='utf-8', newline='\r'), 'abcde\r\nfghlk\n\rmnopq')
# Check that `\r\n` character replaces `\n`
self.assertEqual(p.read_text(encoding='utf-8', newline='\r\n'), 'abcde\r\nfghlk\n\rmnopq')
def test_iterdir(self):
expected = ['dirA', 'dirB', 'dirC', 'fileA']
if self.ground.can_symlink:
expected += ['linkA', 'linkB', 'brokenLink', 'brokenLinkLoop']
expected = {self.root.joinpath(name) for name in expected}
actual = set(self.root.iterdir())
self.assertEqual(actual, expected)
def test_iterdir_nodir(self):
p = self.root / 'fileA'
self.assertRaises(OSError, p.iterdir)
def test_iterdir_info(self):
for child in self.root.iterdir():
self.assertIsInstance(child.info, PathInfo)
self.assertTrue(child.info.exists(follow_symlinks=False))
def test_glob(self):
if not self.ground.can_symlink:
self.skipTest("requires symlinks")
p = self.root
sep = self.root.parser.sep
altsep = self.root.parser.altsep
def check(pattern, expected):
if altsep:
expected = {name.replace(altsep, sep) for name in expected}
expected = {p.joinpath(name) for name in expected}
actual = set(p.glob(pattern, recurse_symlinks=True))
self.assertEqual(actual, expected)
it = p.glob("fileA")
self.assertIsInstance(it, collections.abc.Iterator)
self.assertEqual(list(it), [p.joinpath("fileA")])
check("*A", ["dirA", "fileA", "linkA"])
check("*A", ['dirA', 'fileA', 'linkA'])
check("*B/*", ["dirB/fileB", "linkB/fileB"])
check("*B/*", ['dirB/fileB', 'linkB/fileB'])
check("brokenLink", ['brokenLink'])
check("brokenLinkLoop", ['brokenLinkLoop'])
check("**/", ["", "dirA/", "dirA/linkC/", "dirB/", "dirC/", "dirC/dirD/", "linkB/"])
check("**/*/", ["dirA/", "dirA/linkC/", "dirB/", "dirC/", "dirC/dirD/", "linkB/"])
check("*/", ["dirA/", "dirB/", "dirC/", "linkB/"])
check("*/dirD/**/", ["dirC/dirD/"])
check("*/dirD/**", ["dirC/dirD/", "dirC/dirD/fileD"])
check("dir*/**", ["dirA/", "dirA/linkC", "dirA/linkC/fileB", "dirB/", "dirB/fileB", "dirC/",
"dirC/fileC", "dirC/dirD", "dirC/dirD/fileD", "dirC/novel.txt"])
check("dir*/**/", ["dirA/", "dirA/linkC/", "dirB/", "dirC/", "dirC/dirD/"])
check("dir*/**/..", ["dirA/..", "dirA/linkC/..", "dirB/..", "dirC/..", "dirC/dirD/.."])
check("dir*/*/**", ["dirA/linkC/", "dirA/linkC/fileB", "dirC/dirD/", "dirC/dirD/fileD"])
check("dir*/*/**/", ["dirA/linkC/", "dirC/dirD/"])
check("dir*/*/**/..", ["dirA/linkC/..", "dirC/dirD/.."])
check("dir*/*/..", ["dirC/dirD/..", "dirA/linkC/.."])
check("dir*/*/../dirD/**/", ["dirC/dirD/../dirD/"])
check("dir*/**/fileC", ["dirC/fileC"])
check("dir*/file*", ["dirB/fileB", "dirC/fileC"])
check("**/*/fileA", [])
check("fileB", [])
check("**/*/fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB"])
check("**/fileB", ["dirB/fileB", "dirA/linkC/fileB", "linkB/fileB"])
check("*/fileB", ["dirB/fileB", "linkB/fileB"])
check("*/fileB", ['dirB/fileB', 'linkB/fileB'])
check("**/file*",
["fileA", "dirA/linkC/fileB", "dirB/fileB", "dirC/fileC", "dirC/dirD/fileD",
"linkB/fileB"])
with self.assertRaisesRegex(ValueError, 'Unacceptable pattern'):
list(p.glob(''))
def test_walk_top_down(self):
it = self.root.walk()
path, dirnames, filenames = next(it)
dirnames.sort()
filenames.sort()
self.assertEqual(path, self.root)
self.assertEqual(dirnames, ['dirA', 'dirB', 'dirC'])
self.assertEqual(filenames, ['brokenLink', 'brokenLinkLoop', 'fileA', 'linkA', 'linkB']
if self.ground.can_symlink else ['fileA'])
path, dirnames, filenames = next(it)
self.assertEqual(path, self.root / 'dirA')
self.assertEqual(dirnames, [])
self.assertEqual(filenames, ['linkC'] if self.ground.can_symlink else [])
path, dirnames, filenames = next(it)
self.assertEqual(path, self.root / 'dirB')
self.assertEqual(dirnames, [])
self.assertEqual(filenames, ['fileB'])
path, dirnames, filenames = next(it)
filenames.sort()
self.assertEqual(path, self.root / 'dirC')
self.assertEqual(dirnames, ['dirD'])
self.assertEqual(filenames, ['fileC', 'novel.txt'])
path, dirnames, filenames = next(it)
self.assertEqual(path, self.root / 'dirC' / 'dirD')
self.assertEqual(dirnames, [])
self.assertEqual(filenames, ['fileD'])
self.assertRaises(StopIteration, next, it)
def test_walk_prune(self):
expected = {self.root, self.root / 'dirA', self.root / 'dirC', self.root / 'dirC' / 'dirD'}
actual = set()
for path, dirnames, filenames in self.root.walk():
actual.add(path)
if path == self.root:
dirnames.remove('dirB')
self.assertEqual(actual, expected)
def test_walk_bottom_up(self):
seen_root = seen_dira = seen_dirb = seen_dirc = seen_dird = False
for path, dirnames, filenames in self.root.walk(top_down=False):
if path == self.root:
self.assertFalse(seen_root)
self.assertTrue(seen_dira)
self.assertTrue(seen_dirb)
self.assertTrue(seen_dirc)
self.assertEqual(sorted(dirnames), ['dirA', 'dirB', 'dirC'])
self.assertEqual(sorted(filenames),
['brokenLink', 'brokenLinkLoop', 'fileA', 'linkA', 'linkB']
if self.ground.can_symlink else ['fileA'])
seen_root = True
elif path == self.root / 'dirA':
self.assertFalse(seen_root)
self.assertFalse(seen_dira)
self.assertEqual(dirnames, [])
self.assertEqual(filenames, ['linkC'] if self.ground.can_symlink else [])
seen_dira = True
elif path == self.root / 'dirB':
self.assertFalse(seen_root)
self.assertFalse(seen_dirb)
self.assertEqual(dirnames, [])
self.assertEqual(filenames, ['fileB'])
seen_dirb = True
elif path == self.root / 'dirC':
self.assertFalse(seen_root)
self.assertFalse(seen_dirc)
self.assertTrue(seen_dird)
self.assertEqual(dirnames, ['dirD'])
self.assertEqual(sorted(filenames), ['fileC', 'novel.txt'])
seen_dirc = True
elif path == self.root / 'dirC' / 'dirD':
self.assertFalse(seen_root)
self.assertFalse(seen_dirc)
self.assertFalse(seen_dird)
self.assertEqual(dirnames, [])
self.assertEqual(filenames, ['fileD'])
seen_dird = True
else:
raise AssertionError(f"Unexpected path: {path}")
self.assertTrue(seen_root)
def test_info_exists(self):
p = self.root
self.assertTrue(p.info.exists())
self.assertTrue((p / 'dirA').info.exists())
self.assertTrue((p / 'dirA').info.exists(follow_symlinks=False))
self.assertTrue((p / 'fileA').info.exists())
self.assertTrue((p / 'fileA').info.exists(follow_symlinks=False))
self.assertFalse((p / 'non-existing').info.exists())
self.assertFalse((p / 'non-existing').info.exists(follow_symlinks=False))
if self.ground.can_symlink:
self.assertTrue((p / 'linkA').info.exists())
self.assertTrue((p / 'linkA').info.exists(follow_symlinks=False))
self.assertTrue((p / 'linkB').info.exists())
self.assertTrue((p / 'linkB').info.exists(follow_symlinks=True))
self.assertFalse((p / 'brokenLink').info.exists())
self.assertTrue((p / 'brokenLink').info.exists(follow_symlinks=False))
self.assertFalse((p / 'brokenLinkLoop').info.exists())
self.assertTrue((p / 'brokenLinkLoop').info.exists(follow_symlinks=False))
self.assertFalse((p / 'fileA\udfff').info.exists())
self.assertFalse((p / 'fileA\udfff').info.exists(follow_symlinks=False))
self.assertFalse((p / 'fileA\x00').info.exists())
self.assertFalse((p / 'fileA\x00').info.exists(follow_symlinks=False))
def test_info_is_dir(self):
p = self.root
self.assertTrue((p / 'dirA').info.is_dir())
self.assertTrue((p / 'dirA').info.is_dir(follow_symlinks=False))
self.assertFalse((p / 'fileA').info.is_dir())
self.assertFalse((p / 'fileA').info.is_dir(follow_symlinks=False))
self.assertFalse((p / 'non-existing').info.is_dir())
self.assertFalse((p / 'non-existing').info.is_dir(follow_symlinks=False))
if self.ground.can_symlink:
self.assertFalse((p / 'linkA').info.is_dir())
self.assertFalse((p / 'linkA').info.is_dir(follow_symlinks=False))
self.assertTrue((p / 'linkB').info.is_dir())
self.assertFalse((p / 'linkB').info.is_dir(follow_symlinks=False))
self.assertFalse((p / 'brokenLink').info.is_dir())
self.assertFalse((p / 'brokenLink').info.is_dir(follow_symlinks=False))
self.assertFalse((p / 'brokenLinkLoop').info.is_dir())
self.assertFalse((p / 'brokenLinkLoop').info.is_dir(follow_symlinks=False))
self.assertFalse((p / 'dirA\udfff').info.is_dir())
self.assertFalse((p / 'dirA\udfff').info.is_dir(follow_symlinks=False))
self.assertFalse((p / 'dirA\x00').info.is_dir())
self.assertFalse((p / 'dirA\x00').info.is_dir(follow_symlinks=False))
def test_info_is_file(self):
p = self.root
self.assertTrue((p / 'fileA').info.is_file())
self.assertTrue((p / 'fileA').info.is_file(follow_symlinks=False))
self.assertFalse((p / 'dirA').info.is_file())
self.assertFalse((p / 'dirA').info.is_file(follow_symlinks=False))
self.assertFalse((p / 'non-existing').info.is_file())
self.assertFalse((p / 'non-existing').info.is_file(follow_symlinks=False))
if self.ground.can_symlink:
self.assertTrue((p / 'linkA').info.is_file())
self.assertFalse((p / 'linkA').info.is_file(follow_symlinks=False))
self.assertFalse((p / 'linkB').info.is_file())
self.assertFalse((p / 'linkB').info.is_file(follow_symlinks=False))
self.assertFalse((p / 'brokenLink').info.is_file())
self.assertFalse((p / 'brokenLink').info.is_file(follow_symlinks=False))
self.assertFalse((p / 'brokenLinkLoop').info.is_file())
self.assertFalse((p / 'brokenLinkLoop').info.is_file(follow_symlinks=False))
self.assertFalse((p / 'fileA\udfff').info.is_file())
self.assertFalse((p / 'fileA\udfff').info.is_file(follow_symlinks=False))
self.assertFalse((p / 'fileA\x00').info.is_file())
self.assertFalse((p / 'fileA\x00').info.is_file(follow_symlinks=False))
def test_info_is_symlink(self):
p = self.root
self.assertFalse((p / 'fileA').info.is_symlink())
self.assertFalse((p / 'dirA').info.is_symlink())
self.assertFalse((p / 'non-existing').info.is_symlink())
if self.ground.can_symlink:
self.assertTrue((p / 'linkA').info.is_symlink())
self.assertTrue((p / 'linkB').info.is_symlink())
self.assertTrue((p / 'brokenLink').info.is_symlink())
self.assertFalse((p / 'linkA\udfff').info.is_symlink())
self.assertFalse((p / 'linkA\x00').info.is_symlink())
self.assertTrue((p / 'brokenLinkLoop').info.is_symlink())
self.assertFalse((p / 'fileA\udfff').info.is_symlink())
self.assertFalse((p / 'fileA\x00').info.is_symlink())
class ZipPathReadTest(ReadTestBase, unittest.TestCase):
ground = ZipPathGround(ReadableZipPath)
class LocalPathReadTest(ReadTestBase, unittest.TestCase):
ground = LocalPathGround(ReadableLocalPath)
if not is_pypi:
from pathlib import Path
class PathReadTest(ReadTestBase, unittest.TestCase):
ground = LocalPathGround(Path)
if __name__ == "__main__":
unittest.main()
+143
View File
@@ -0,0 +1,143 @@
"""
Tests for pathlib.types._WritablePath
"""
import io
import os
import sys
import unittest
from .support import is_pypi
from .support.local_path import WritableLocalPath, LocalPathGround
from .support.zip_path import WritableZipPath, ZipPathGround
if is_pypi:
from pathlib_abc import _WritablePath
from pathlib_abc._os import magic_open
else:
from pathlib.types import _WritablePath
from pathlib._os import magic_open
class WriteTestBase:
def setUp(self):
self.root = self.ground.setup()
def tearDown(self):
self.ground.teardown(self.root)
def test_is_writable(self):
self.assertIsInstance(self.root, _WritablePath)
def test_open_w(self):
p = self.root / 'fileA'
with magic_open(p, 'w', encoding='utf-8') as f:
self.assertIsInstance(f, io.TextIOBase)
f.write('this is file A\n')
self.assertEqual(self.ground.readtext(p), 'this is file A\n')
@unittest.skipIf(
not getattr(sys.flags, 'warn_default_encoding', 0),
"Requires warn_default_encoding",
)
def test_open_w_encoding_warning(self):
p = self.root / 'fileA'
with self.assertWarns(EncodingWarning) as wc:
with magic_open(p, 'w'):
pass
self.assertEqual(wc.filename, __file__)
def test_open_wb(self):
p = self.root / 'fileA'
with magic_open(p, 'wb') as f:
#self.assertIsInstance(f, io.BufferedWriter)
f.write(b'this is file A\n')
self.assertEqual(self.ground.readbytes(p), b'this is file A\n')
self.assertRaises(ValueError, magic_open, p, 'wb', encoding='utf8')
self.assertRaises(ValueError, magic_open, p, 'wb', errors='strict')
self.assertRaises(ValueError, magic_open, p, 'wb', newline='')
def test_write_bytes(self):
p = self.root / 'fileA'
data = b'abcdefg'
self.assertEqual(len(data), p.write_bytes(data))
self.assertEqual(self.ground.readbytes(p), data)
# Check that trying to write str does not truncate the file.
self.assertRaises(TypeError, p.write_bytes, 'somestr')
self.assertEqual(self.ground.readbytes(p), data)
def test_write_text(self):
p = self.root / 'fileA'
data = 'äbcdefg'
self.assertEqual(len(data), p.write_text(data, encoding='latin-1'))
self.assertEqual(self.ground.readbytes(p), b'\xe4bcdefg')
# Check that trying to write bytes does not truncate the file.
self.assertRaises(TypeError, p.write_text, b'somebytes', encoding='utf-8')
self.assertEqual(self.ground.readbytes(p), b'\xe4bcdefg')
@unittest.skipIf(
not getattr(sys.flags, 'warn_default_encoding', 0),
"Requires warn_default_encoding",
)
def test_write_text_encoding_warning(self):
p = self.root / 'fileA'
with self.assertWarns(EncodingWarning) as wc:
p.write_text('abcdefg')
self.assertEqual(wc.filename, __file__)
def test_write_text_with_newlines(self):
# Check that `\n` character change nothing
p = self.root / 'fileA'
p.write_text('abcde\r\nfghlk\n\rmnopq', encoding='utf-8', newline='\n')
self.assertEqual(self.ground.readbytes(p), b'abcde\r\nfghlk\n\rmnopq')
# Check that `\r` character replaces `\n`
p = self.root / 'fileB'
p.write_text('abcde\r\nfghlk\n\rmnopq', encoding='utf-8', newline='\r')
self.assertEqual(self.ground.readbytes(p), b'abcde\r\rfghlk\r\rmnopq')
# Check that `\r\n` character replaces `\n`
p = self.root / 'fileC'
p.write_text('abcde\r\nfghlk\n\rmnopq', encoding='utf-8', newline='\r\n')
self.assertEqual(self.ground.readbytes(p), b'abcde\r\r\nfghlk\r\n\rmnopq')
# Check that no argument passed will change `\n` to `os.linesep`
os_linesep_byte = bytes(os.linesep, encoding='ascii')
p = self.root / 'fileD'
p.write_text('abcde\nfghlk\n\rmnopq', encoding='utf-8')
self.assertEqual(self.ground.readbytes(p),
b'abcde' + os_linesep_byte +
b'fghlk' + os_linesep_byte + b'\rmnopq')
def test_mkdir(self):
p = self.root / 'newdirA'
self.assertFalse(self.ground.isdir(p))
p.mkdir()
self.assertTrue(self.ground.isdir(p))
def test_symlink_to(self):
if not self.ground.can_symlink:
self.skipTest('needs symlinks')
link = self.root.joinpath('linkA')
link.symlink_to('fileA')
self.assertTrue(self.ground.islink(link))
self.assertEqual(self.ground.readlink(link), 'fileA')
class ZipPathWriteTest(WriteTestBase, unittest.TestCase):
ground = ZipPathGround(WritableZipPath)
class LocalPathWriteTest(WriteTestBase, unittest.TestCase):
ground = LocalPathGround(WritableLocalPath)
if not is_pypi:
from pathlib import Path
class PathWriteTest(WriteTestBase, unittest.TestCase):
ground = LocalPathGround(Path)
if __name__ == "__main__":
unittest.main()