mirror of
				https://github.com/python/cpython.git
				synced 2025-10-26 11:14:33 +00:00 
			
		
		
		
	GH-128520: Make pathlib._abc.WritablePath a sibling of ReadablePath (#129014)
				
					
				
			In the private pathlib ABCs, support write-only virtual filesystems by making `WritablePath` inherit directly from `JoinablePath`, rather than subclassing `ReadablePath`. There are two complications: - `ReadablePath.open()` applies to both reading and writing - `ReadablePath.copy` is secretly an object that supports the *read* side of copying, whereas `WritablePath.copy` is a different kind of object supporting the *write* side We untangle these as follow: - A new `pathlib._abc.magic_open()` function replaces the `open()` method, which is dropped from the ABCs but remains in `pathlib.Path`. The function works like `io.open()`, but additionally accepts objects with `__open_rb__()` or `__open_wb__()` methods as appropriate for the mode. These new dunders are made abstract methods of `ReadablePath` and `WritablePath` respectively. If the pathlib ABCs are made public, we could consider blessing an "openable" protocol and supporting it in `io.open()`, removing the need for `pathlib._abc.magic_open()`. - `ReadablePath.copy` becomes a true method, whereas `WritablePath.copy` is deleted. A new `ReadablePath._copy_reader` property provides a `CopyReader` object, and similarly `WritablePath._copy_writer` is a `CopyWriter` object. Once GH-125413 is resolved, we'll be able to move the `CopyReader` functionality into `ReadablePath.info` and eliminate `ReadablePath._copy_reader`.
This commit is contained in:
		
							parent
							
								
									3d7c0e5366
								
							
						
					
					
						commit
						01d91500ca
					
				
					 4 changed files with 178 additions and 115 deletions
				
			
		|  | @ -4,7 +4,7 @@ | |||
| import errno | ||||
| import unittest | ||||
| 
 | ||||
| from pathlib._abc import JoinablePath, ReadablePath, WritablePath | ||||
| from pathlib._abc import JoinablePath, ReadablePath, WritablePath, magic_open | ||||
| from pathlib._types import Parser | ||||
| import posixpath | ||||
| 
 | ||||
|  | @ -918,7 +918,7 @@ def test_with_suffix_invalid(self): | |||
| 
 | ||||
| class DummyWritablePathIO(io.BytesIO): | ||||
|     """ | ||||
|     Used by DummyWritablePath to implement `open('w')` | ||||
|     Used by DummyWritablePath to implement `__open_wb__()` | ||||
|     """ | ||||
| 
 | ||||
|     def __init__(self, files, path): | ||||
|  | @ -931,38 +931,16 @@ def close(self): | |||
|         super().close() | ||||
| 
 | ||||
| 
 | ||||
| class DummyReadablePath(ReadablePath): | ||||
| class DummyReadablePath(ReadablePath, DummyJoinablePath): | ||||
|     """ | ||||
|     Simple implementation of DummyReadablePath that keeps files and | ||||
|     directories in memory. | ||||
|     """ | ||||
|     __slots__ = ('_segments') | ||||
|     __slots__ = () | ||||
| 
 | ||||
|     _files = {} | ||||
|     _directories = {} | ||||
| 
 | ||||
|     def __init__(self, *segments): | ||||
|         self._segments = segments | ||||
| 
 | ||||
|     def __str__(self): | ||||
|         if self._segments: | ||||
|             return self.parser.join(*self._segments) | ||||
|         return '' | ||||
| 
 | ||||
|     def __eq__(self, other): | ||||
|         if not isinstance(other, DummyReadablePath): | ||||
|             return NotImplemented | ||||
|         return str(self) == str(other) | ||||
| 
 | ||||
|     def __hash__(self): | ||||
|         return hash(str(self)) | ||||
| 
 | ||||
|     def __repr__(self): | ||||
|         return "{}({!r})".format(self.__class__.__name__, str(self)) | ||||
| 
 | ||||
|     def with_segments(self, *pathsegments): | ||||
|         return type(self)(*pathsegments) | ||||
| 
 | ||||
|     def exists(self, *, follow_symlinks=True): | ||||
|         return self.is_dir() or self.is_file() | ||||
| 
 | ||||
|  | @ -975,33 +953,13 @@ def is_file(self, *, follow_symlinks=True): | |||
|     def is_symlink(self): | ||||
|         return False | ||||
| 
 | ||||
|     def open(self, mode='r', buffering=-1, encoding=None, | ||||
|              errors=None, newline=None): | ||||
|         if buffering != -1 and not (buffering == 0 and 'b' in mode): | ||||
|             raise NotImplementedError | ||||
|     def __open_rb__(self, buffering=-1): | ||||
|         path = str(self) | ||||
|         if path in self._directories: | ||||
|             raise IsADirectoryError(errno.EISDIR, "Is a directory", path) | ||||
| 
 | ||||
|         text = 'b' not in mode | ||||
|         mode = ''.join(c for c in mode if c not in 'btU') | ||||
|         if mode == 'r': | ||||
|             if path not in self._files: | ||||
|                 raise FileNotFoundError(errno.ENOENT, "File not found", path) | ||||
|             stream = io.BytesIO(self._files[path]) | ||||
|         elif mode == 'w': | ||||
|             # FIXME: move to DummyWritablePath | ||||
|             parent, name = posixpath.split(path) | ||||
|             if parent not in self._directories: | ||||
|                 raise FileNotFoundError(errno.ENOENT, "File not found", parent) | ||||
|             stream = DummyWritablePathIO(self._files, path) | ||||
|             self._files[path] = b'' | ||||
|             self._directories[parent].add(name) | ||||
|         else: | ||||
|             raise NotImplementedError | ||||
|         if text: | ||||
|             stream = io.TextIOWrapper(stream, encoding=encoding, errors=errors, newline=newline) | ||||
|         return stream | ||||
|         elif path not in self._files: | ||||
|             raise FileNotFoundError(errno.ENOENT, "File not found", path) | ||||
|         return io.BytesIO(self._files[path]) | ||||
| 
 | ||||
|     def iterdir(self): | ||||
|         path = str(self).rstrip('/') | ||||
|  | @ -1013,9 +971,20 @@ def iterdir(self): | |||
|             raise FileNotFoundError(errno.ENOENT, "File not found", path) | ||||
| 
 | ||||
| 
 | ||||
| class DummyWritablePath(DummyReadablePath, WritablePath): | ||||
| class DummyWritablePath(WritablePath, DummyJoinablePath): | ||||
|     __slots__ = () | ||||
| 
 | ||||
|     def __open_wb__(self, buffering=-1): | ||||
|         path = str(self) | ||||
|         if path in self._directories: | ||||
|             raise IsADirectoryError(errno.EISDIR, "Is a directory", path) | ||||
|         parent, name = posixpath.split(path) | ||||
|         if parent not in self._directories: | ||||
|             raise FileNotFoundError(errno.ENOENT, "File not found", parent) | ||||
|         self._files[path] = b'' | ||||
|         self._directories[parent].add(name) | ||||
|         return DummyWritablePathIO(self._files, path) | ||||
| 
 | ||||
|     def mkdir(self, mode=0o777, parents=False, exist_ok=False): | ||||
|         path = str(self) | ||||
|         parent = str(self.parent) | ||||
|  | @ -1121,12 +1090,12 @@ def test_exists(self): | |||
|         self.assertIs(False, P(self.base + '\udfff').exists()) | ||||
|         self.assertIs(False, P(self.base + '\x00').exists()) | ||||
| 
 | ||||
|     def test_open_common(self): | ||||
|     def test_magic_open(self): | ||||
|         p = self.cls(self.base) | ||||
|         with (p / 'fileA').open('r') as f: | ||||
|         with magic_open(p / 'fileA', 'r') as f: | ||||
|             self.assertIsInstance(f, io.TextIOBase) | ||||
|             self.assertEqual(f.read(), "this is file A\n") | ||||
|         with (p / 'fileA').open('rb') as f: | ||||
|         with magic_open(p / 'fileA', 'rb') as f: | ||||
|             self.assertIsInstance(f, io.BufferedIOBase) | ||||
|             self.assertEqual(f.read().strip(), b"this is file A") | ||||
| 
 | ||||
|  | @ -1359,9 +1328,18 @@ def test_is_symlink(self): | |||
|             self.assertIs((P / 'linkA\x00').is_file(), False) | ||||
| 
 | ||||
| 
 | ||||
| class DummyWritablePathTest(DummyReadablePathTest): | ||||
| class DummyWritablePathTest(DummyJoinablePathTest): | ||||
|     cls = DummyWritablePath | ||||
| 
 | ||||
| 
 | ||||
| class DummyRWPath(DummyWritablePath, DummyReadablePath): | ||||
|     __slots__ = () | ||||
| 
 | ||||
| 
 | ||||
| class DummyRWPathTest(DummyWritablePathTest, DummyReadablePathTest): | ||||
|     cls = DummyRWPath | ||||
|     can_symlink = False | ||||
| 
 | ||||
|     def test_read_write_bytes(self): | ||||
|         p = self.cls(self.base) | ||||
|         (p / 'fileA').write_bytes(b'abcdefg') | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Barney Gale
						Barney Gale