mirror of
				https://github.com/python/cpython.git
				synced 2025-10-31 13:41:24 +00:00 
			
		
		
		
	GH-128520: Merge pathlib._abc into pathlib.types (#130747)
				
					
				
			There used to be a meaningful distinction between these modules: `pathlib` imported `pathlib._abc` but not `pathlib.types`. This is no longer the case (neither module is imported), so we move the ABCs as follows: - `pathlib._abc.JoinablePath` --> `pathlib.types._JoinablePath` - `pathlib._abc.ReadablePath` --> `pathlib.types._ReadablePath` - `pathlib._abc.WritablePath` --> `pathlib.types._WritablePath`
This commit is contained in:
		
							parent
							
								
									321bf59512
								
							
						
					
					
						commit
						d0eb01c9de
					
				
					 4 changed files with 406 additions and 410 deletions
				
			
		|  | @ -1,397 +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. |  | ||||||
| 
 |  | ||||||
| Three base classes are defined here -- JoinablePath, ReadablePath and |  | ||||||
| WritablePath. |  | ||||||
| """ |  | ||||||
| 
 |  | ||||||
| from abc import ABC, abstractmethod |  | ||||||
| from glob import _PathGlobber, _no_recurse_symlinks |  | ||||||
| from pathlib import PurePath, Path |  | ||||||
| from pathlib._os import magic_open, ensure_distinct_paths, copy_file |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| def _explode_path(path): |  | ||||||
|     """ |  | ||||||
|     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 = path.parser.split |  | ||||||
|     path = str(path) |  | ||||||
|     parent, name = split(path) |  | ||||||
|     names = [] |  | ||||||
|     while path != parent: |  | ||||||
|         names.append(name) |  | ||||||
|         path = parent |  | ||||||
|         parent, name = split(path) |  | ||||||
|     return path, names |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 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(self)[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}") |  | ||||||
|         return self.with_segments(split(str(self))[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('.'): |  | ||||||
|             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(self) |  | ||||||
|         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, *, case_sensitive=None): |  | ||||||
|         """ |  | ||||||
|         Return True if this path matches the given glob-style pattern. The |  | ||||||
|         pattern is matched against the entire path. |  | ||||||
|         """ |  | ||||||
|         if not hasattr(pattern, 'with_segments'): |  | ||||||
|             pattern = self.with_segments(pattern) |  | ||||||
|         if case_sensitive is None: |  | ||||||
|             case_sensitive = self.parser.normcase('Aa') == 'Aa' |  | ||||||
|         globber = _PathGlobber(pattern.parser.sep, case_sensitive, recursive=True) |  | ||||||
|         match = globber.compile(str(pattern)) |  | ||||||
|         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. |  | ||||||
|         """ |  | ||||||
|         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, *, 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 hasattr(pattern, 'with_segments'): |  | ||||||
|             pattern = self.with_segments(pattern) |  | ||||||
|         anchor, parts = _explode_path(pattern) |  | ||||||
|         if anchor: |  | ||||||
|             raise NotImplementedError("Non-relative patterns are unsupported") |  | ||||||
|         case_sensitive_default = self.parser.normcase('Aa') == 'Aa' |  | ||||||
|         if case_sensitive is None: |  | ||||||
|             case_sensitive = case_sensitive_default |  | ||||||
|             case_pedantic = False |  | ||||||
|         else: |  | ||||||
|             case_pedantic = case_sensitive_default != case_sensitive |  | ||||||
|         recursive = True if recurse_symlinks else _no_recurse_symlinks |  | ||||||
|         globber = _PathGlobber(self.parser.sep, case_sensitive, case_pedantic, recursive) |  | ||||||
|         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, follow_symlinks=True, preserve_metadata=False): |  | ||||||
|         """ |  | ||||||
|         Recursively copy this file or directory tree to the given destination. |  | ||||||
|         """ |  | ||||||
|         if not hasattr(target, 'with_segments'): |  | ||||||
|             target = self.with_segments(target) |  | ||||||
|         ensure_distinct_paths(self, target) |  | ||||||
|         copy_file(self, target, follow_symlinks, preserve_metadata) |  | ||||||
|         return target.joinpath()  # Empty join to ensure fresh metadata. |  | ||||||
| 
 |  | ||||||
|     def copy_into(self, target_dir, *, follow_symlinks=True, |  | ||||||
|                   preserve_metadata=False): |  | ||||||
|         """ |  | ||||||
|         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") |  | ||||||
|         elif hasattr(target_dir, 'with_segments'): |  | ||||||
|             target = target_dir / name |  | ||||||
|         else: |  | ||||||
|             target = self.with_segments(target_dir, name) |  | ||||||
|         return self.copy(target, follow_symlinks=follow_symlinks, |  | ||||||
|                          preserve_metadata=preserve_metadata) |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| 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. |  | ||||||
|         """ |  | ||||||
|         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 _write_info(self, info, follow_symlinks=True): |  | ||||||
|         """ |  | ||||||
|         Write the given PathInfo to this path. |  | ||||||
|         """ |  | ||||||
|         pass |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| JoinablePath.register(PurePath) |  | ||||||
| ReadablePath.register(Path) |  | ||||||
| WritablePath.register(Path) |  | ||||||
|  | @ -1,9 +1,39 @@ | ||||||
| """ | """ | ||||||
| Protocols for supporting classes in pathlib. | 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, _no_recurse_symlinks | ||||||
|  | from pathlib import PurePath, Path | ||||||
|  | from pathlib._os import magic_open, ensure_distinct_paths, copy_file | ||||||
| from typing import Protocol, runtime_checkable | from typing import Protocol, runtime_checkable | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | def _explode_path(path): | ||||||
|  |     """ | ||||||
|  |     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 = path.parser.split | ||||||
|  |     path = str(path) | ||||||
|  |     parent, name = split(path) | ||||||
|  |     names = [] | ||||||
|  |     while path != parent: | ||||||
|  |         names.append(name) | ||||||
|  |         path = parent | ||||||
|  |         parent, name = split(path) | ||||||
|  |     return path, names | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @runtime_checkable | @runtime_checkable | ||||||
| class _PathParser(Protocol): | class _PathParser(Protocol): | ||||||
|     """Protocol for path parsers, which do low-level path manipulation. |     """Protocol for path parsers, which do low-level path manipulation. | ||||||
|  | @ -28,3 +58,366 @@ def exists(self, *, follow_symlinks: bool = True) -> bool: ... | ||||||
|     def is_dir(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_file(self, *, follow_symlinks: bool = True) -> bool: ... | ||||||
|     def is_symlink(self) -> 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(self)[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}") | ||||||
|  |         return self.with_segments(split(str(self))[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('.'): | ||||||
|  |             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(self) | ||||||
|  |         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, *, case_sensitive=None): | ||||||
|  |         """ | ||||||
|  |         Return True if this path matches the given glob-style pattern. The | ||||||
|  |         pattern is matched against the entire path. | ||||||
|  |         """ | ||||||
|  |         if not hasattr(pattern, 'with_segments'): | ||||||
|  |             pattern = self.with_segments(pattern) | ||||||
|  |         if case_sensitive is None: | ||||||
|  |             case_sensitive = self.parser.normcase('Aa') == 'Aa' | ||||||
|  |         globber = _PathGlobber(pattern.parser.sep, case_sensitive, recursive=True) | ||||||
|  |         match = globber.compile(str(pattern)) | ||||||
|  |         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. | ||||||
|  |         """ | ||||||
|  |         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, *, 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 hasattr(pattern, 'with_segments'): | ||||||
|  |             pattern = self.with_segments(pattern) | ||||||
|  |         anchor, parts = _explode_path(pattern) | ||||||
|  |         if anchor: | ||||||
|  |             raise NotImplementedError("Non-relative patterns are unsupported") | ||||||
|  |         case_sensitive_default = self.parser.normcase('Aa') == 'Aa' | ||||||
|  |         if case_sensitive is None: | ||||||
|  |             case_sensitive = case_sensitive_default | ||||||
|  |             case_pedantic = False | ||||||
|  |         else: | ||||||
|  |             case_pedantic = case_sensitive_default != case_sensitive | ||||||
|  |         recursive = True if recurse_symlinks else _no_recurse_symlinks | ||||||
|  |         globber = _PathGlobber(self.parser.sep, case_sensitive, case_pedantic, recursive) | ||||||
|  |         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, follow_symlinks=True, preserve_metadata=False): | ||||||
|  |         """ | ||||||
|  |         Recursively copy this file or directory tree to the given destination. | ||||||
|  |         """ | ||||||
|  |         if not hasattr(target, 'with_segments'): | ||||||
|  |             target = self.with_segments(target) | ||||||
|  |         ensure_distinct_paths(self, target) | ||||||
|  |         copy_file(self, target, follow_symlinks, preserve_metadata) | ||||||
|  |         return target.joinpath()  # Empty join to ensure fresh metadata. | ||||||
|  | 
 | ||||||
|  |     def copy_into(self, target_dir, *, follow_symlinks=True, | ||||||
|  |                   preserve_metadata=False): | ||||||
|  |         """ | ||||||
|  |         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") | ||||||
|  |         elif hasattr(target_dir, 'with_segments'): | ||||||
|  |             target = target_dir / name | ||||||
|  |         else: | ||||||
|  |             target = self.with_segments(target_dir, name) | ||||||
|  |         return self.copy(target, follow_symlinks=follow_symlinks, | ||||||
|  |                          preserve_metadata=preserve_metadata) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 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. | ||||||
|  |         """ | ||||||
|  |         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 _write_info(self, info, follow_symlinks=True): | ||||||
|  |         """ | ||||||
|  |         Write the given PathInfo to this path. | ||||||
|  |         """ | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | _JoinablePath.register(PurePath) | ||||||
|  | _ReadablePath.register(Path) | ||||||
|  | _WritablePath.register(Path) | ||||||
|  |  | ||||||
|  | @ -1059,14 +1059,14 @@ def tempdir(self): | ||||||
|         return d |         return d | ||||||
| 
 | 
 | ||||||
|     def test_matches_writablepath_docstrings(self): |     def test_matches_writablepath_docstrings(self): | ||||||
|         path_names = {name for name in dir(pathlib._abc.WritablePath) if name[0] != '_'} |         path_names = {name for name in dir(pathlib.types._WritablePath) if name[0] != '_'} | ||||||
|         for attr_name in path_names: |         for attr_name in path_names: | ||||||
|             if attr_name == 'parser': |             if attr_name == 'parser': | ||||||
|                 # On Windows, Path.parser is ntpath, but WritablePath.parser is |                 # On Windows, Path.parser is ntpath, but WritablePath.parser is | ||||||
|                 # posixpath, and so their docstrings differ. |                 # posixpath, and so their docstrings differ. | ||||||
|                 continue |                 continue | ||||||
|             our_attr = getattr(self.cls, attr_name) |             our_attr = getattr(self.cls, attr_name) | ||||||
|             path_attr = getattr(pathlib._abc.WritablePath, attr_name) |             path_attr = getattr(pathlib.types._WritablePath, attr_name) | ||||||
|             self.assertEqual(our_attr.__doc__, path_attr.__doc__) |             self.assertEqual(our_attr.__doc__, path_attr.__doc__) | ||||||
| 
 | 
 | ||||||
|     def test_concrete_class(self): |     def test_concrete_class(self): | ||||||
|  |  | ||||||
|  | @ -4,8 +4,8 @@ | ||||||
| import errno | import errno | ||||||
| import unittest | import unittest | ||||||
| 
 | 
 | ||||||
| from pathlib._abc import JoinablePath, ReadablePath, WritablePath, magic_open | from pathlib._os import magic_open | ||||||
| from pathlib.types import _PathParser, PathInfo | from pathlib.types import _PathParser, PathInfo, _JoinablePath, _ReadablePath, _WritablePath | ||||||
| import posixpath | import posixpath | ||||||
| 
 | 
 | ||||||
| from test.support.os_helper import TESTFN | from test.support.os_helper import TESTFN | ||||||
|  | @ -31,7 +31,7 @@ def needs_windows(fn): | ||||||
| # | # | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DummyJoinablePath(JoinablePath): | class DummyJoinablePath(_JoinablePath): | ||||||
|     __slots__ = ('_segments',) |     __slots__ = ('_segments',) | ||||||
| 
 | 
 | ||||||
|     parser = posixpath |     parser = posixpath | ||||||
|  | @ -78,7 +78,7 @@ def setUp(self): | ||||||
| 
 | 
 | ||||||
|     def test_is_joinable(self): |     def test_is_joinable(self): | ||||||
|         p = self.cls(self.base) |         p = self.cls(self.base) | ||||||
|         self.assertIsInstance(p, JoinablePath) |         self.assertIsInstance(p, _JoinablePath) | ||||||
| 
 | 
 | ||||||
|     def test_parser(self): |     def test_parser(self): | ||||||
|         self.assertIsInstance(self.cls.parser, _PathParser) |         self.assertIsInstance(self.cls.parser, _PathParser) | ||||||
|  | @ -855,7 +855,7 @@ def is_symlink(self): | ||||||
|         return False |         return False | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DummyReadablePath(ReadablePath, DummyJoinablePath): | class DummyReadablePath(_ReadablePath, DummyJoinablePath): | ||||||
|     """ |     """ | ||||||
|     Simple implementation of DummyReadablePath that keeps files and |     Simple implementation of DummyReadablePath that keeps files and | ||||||
|     directories in memory. |     directories in memory. | ||||||
|  | @ -900,7 +900,7 @@ def readlink(self): | ||||||
|         raise NotImplementedError |         raise NotImplementedError | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DummyWritablePath(WritablePath, DummyJoinablePath): | class DummyWritablePath(_WritablePath, DummyJoinablePath): | ||||||
|     __slots__ = () |     __slots__ = () | ||||||
| 
 | 
 | ||||||
|     def __open_wb__(self, buffering=-1): |     def __open_wb__(self, buffering=-1): | ||||||
|  | @ -999,7 +999,7 @@ def assertEqualNormCase(self, path_a, path_b): | ||||||
| 
 | 
 | ||||||
|     def test_is_readable(self): |     def test_is_readable(self): | ||||||
|         p = self.cls(self.base) |         p = self.cls(self.base) | ||||||
|         self.assertIsInstance(p, ReadablePath) |         self.assertIsInstance(p, _ReadablePath) | ||||||
| 
 | 
 | ||||||
|     def test_magic_open(self): |     def test_magic_open(self): | ||||||
|         p = self.cls(self.base) |         p = self.cls(self.base) | ||||||
|  | @ -1130,7 +1130,7 @@ def test_info_exists_caching(self): | ||||||
|         q = p / 'myfile' |         q = p / 'myfile' | ||||||
|         self.assertFalse(q.info.exists()) |         self.assertFalse(q.info.exists()) | ||||||
|         self.assertFalse(q.info.exists(follow_symlinks=False)) |         self.assertFalse(q.info.exists(follow_symlinks=False)) | ||||||
|         if isinstance(self.cls, WritablePath): |         if isinstance(self.cls, _WritablePath): | ||||||
|             q.write_text('hullo') |             q.write_text('hullo') | ||||||
|             self.assertFalse(q.info.exists()) |             self.assertFalse(q.info.exists()) | ||||||
|             self.assertFalse(q.info.exists(follow_symlinks=False)) |             self.assertFalse(q.info.exists(follow_symlinks=False)) | ||||||
|  | @ -1162,7 +1162,7 @@ def test_info_is_dir_caching(self): | ||||||
|         q = p / 'mydir' |         q = p / 'mydir' | ||||||
|         self.assertFalse(q.info.is_dir()) |         self.assertFalse(q.info.is_dir()) | ||||||
|         self.assertFalse(q.info.is_dir(follow_symlinks=False)) |         self.assertFalse(q.info.is_dir(follow_symlinks=False)) | ||||||
|         if isinstance(self.cls, WritablePath): |         if isinstance(self.cls, _WritablePath): | ||||||
|             q.mkdir() |             q.mkdir() | ||||||
|             self.assertFalse(q.info.is_dir()) |             self.assertFalse(q.info.is_dir()) | ||||||
|             self.assertFalse(q.info.is_dir(follow_symlinks=False)) |             self.assertFalse(q.info.is_dir(follow_symlinks=False)) | ||||||
|  | @ -1194,7 +1194,7 @@ def test_info_is_file_caching(self): | ||||||
|         q = p / 'myfile' |         q = p / 'myfile' | ||||||
|         self.assertFalse(q.info.is_file()) |         self.assertFalse(q.info.is_file()) | ||||||
|         self.assertFalse(q.info.is_file(follow_symlinks=False)) |         self.assertFalse(q.info.is_file(follow_symlinks=False)) | ||||||
|         if isinstance(self.cls, WritablePath): |         if isinstance(self.cls, _WritablePath): | ||||||
|             q.write_text('hullo') |             q.write_text('hullo') | ||||||
|             self.assertFalse(q.info.is_file()) |             self.assertFalse(q.info.is_file()) | ||||||
|             self.assertFalse(q.info.is_file(follow_symlinks=False)) |             self.assertFalse(q.info.is_file(follow_symlinks=False)) | ||||||
|  | @ -1220,7 +1220,7 @@ class WritablePathTest(JoinablePathTest): | ||||||
| 
 | 
 | ||||||
|     def test_is_writable(self): |     def test_is_writable(self): | ||||||
|         p = self.cls(self.base) |         p = self.cls(self.base) | ||||||
|         self.assertIsInstance(p, WritablePath) |         self.assertIsInstance(p, _WritablePath) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class DummyRWPath(DummyWritablePath, DummyReadablePath): | class DummyRWPath(DummyWritablePath, DummyReadablePath): | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Barney Gale
						Barney Gale