pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/python/cpython/commit/805e3368d6d07e58430654d1365283924fdf4143

9af82350aeda.css" /> GH-128520: pathlib ABCs: improve protocol for 'openable' objects (#13… · python/cpython@805e336 · GitHub
Skip to content

Commit 805e336

Browse files
barneygaleencukou
andauthored
GH-128520: pathlib ABCs: improve protocol for 'openable' objects (#134101)
Rename `pathlib._os.magic_open()` to `vfsopen()`. The new name is a bit less abstract, and it aligns with the `vfspath()` method added in 5dbd27d. Per discussion on discourse[^1], adjust `vfsopen()` so that the following methods may be called: - `__open_reader__()` - `__open_writer__(mode)` - `__open_updater__(mode)` These three methods return readable, writable, and full duplex file objects respectively. In the 'writer' method, *mode* is either 'a', 'w' or 'x'. In the 'updater' method, *mode* is either 'r' or 'w'. In the pathlib ABCs, replace `ReadablePath.__open_rb__()` with `__open_reader__()`, and replace `WritablePath.__open_wb__()` with `__open_writer__()`. [^1]: https://discuss.python.org/t/open-able-objects/90238 Co-authored-by: Petr Viktorin <encukou@gmail.com>
1 parent 2e8f64c commit 805e336

File tree

7 files changed

+114
-64
lines changed

7 files changed

+114
-64
lines changed

Lib/pathlib/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
from pathlib._os import (
3030
PathInfo, DirEntryInfo,
31-
magic_open, vfspath,
31+
vfsopen, vfspath,
3232
ensure_different_files, ensure_distinct_paths,
3333
copyfile2, copyfileobj, copy_info,
3434
)
@@ -1129,7 +1129,7 @@ def _copy_from(self, source, follow_symlinks=True, preserve_metadata=False):
11291129

11301130
def _copy_from_file(self, source, preserve_metadata=False):
11311131
ensure_different_files(source, self)
1132-
with magic_open(source, 'rb') as source_f:
1132+
with vfsopen(source, 'rb') as source_f:
11331133
with open(self, 'wb') as target_f:
11341134
copyfileobj(source_f, target_f)
11351135
if preserve_metadata:

Lib/pathlib/_os.py

Lines changed: 66 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -166,48 +166,86 @@ def copyfileobj(source_f, target_f):
166166
write_target(buf)
167167

168168

169-
def magic_open(path, mode='r', buffering=-1, encoding=None, errors=None,
170-
newline=None):
169+
def _open_reader(obj):
170+
cls = type(obj)
171+
try:
172+
open_reader = cls.__open_reader__
173+
except AttributeError:
174+
cls_name = cls.__name__
175+
raise TypeError(f"{cls_name} can't be opened for reading") from None
176+
else:
177+
return open_reader(obj)
178+
179+
180+
def _open_writer(obj, mode):
181+
cls = type(obj)
182+
try:
183+
open_writer = cls.__open_writer__
184+
except AttributeError:
185+
cls_name = cls.__name__
186+
raise TypeError(f"{cls_name} can't be opened for writing") from None
187+
else:
188+
return open_writer(obj, mode)
189+
190+
191+
def _open_updater(obj, mode):
192+
cls = type(obj)
193+
try:
194+
open_updater = cls.__open_updater__
195+
except AttributeError:
196+
cls_name = cls.__name__
197+
raise TypeError(f"{cls_name} can't be opened for updating") from None
198+
else:
199+
return open_updater(obj, mode)
200+
201+
202+
def vfsopen(obj, mode='r', buffering=-1, encoding=None, errors=None,
203+
newline=None):
171204
"""
172205
Open the file pointed to by this path and return a file object, as
173206
the built-in open() function does.
207+
208+
Unlike the built-in open() function, this function additionally accepts
209+
'openable' objects, which are objects with any of these special methods:
210+
211+
__open_reader__()
212+
__open_writer__(mode)
213+
__open_updater__(mode)
214+
215+
'__open_reader__' is called for 'r' mode; '__open_writer__' for 'a', 'w'
216+
and 'x' modes; and '__open_updater__' for 'r+' and 'w+' modes. If text
217+
mode is requested, the result is wrapped in an io.TextIOWrapper object.
174218
"""
219+
if buffering != -1:
220+
raise ValueError("buffer size can't be customized")
175221
text = 'b' not in mode
176222
if text:
177223
# Call io.text_encoding() here to ensure any warning is raised at an
178224
# appropriate stack level.
179225
encoding = text_encoding(encoding)
180226
try:
181-
return open(path, mode, buffering, encoding, errors, newline)
227+
return open(obj, mode, buffering, encoding, errors, newline)
182228
except TypeError:
183229
pass
184-
cls = type(path)
230+
if not text:
231+
if encoding is not None:
232+
raise ValueError("binary mode doesn't take an encoding argument")
233+
if errors is not None:
234+
raise ValueError("binary mode doesn't take an errors argument")
235+
if newline is not None:
236+
raise ValueError("binary mode doesn't take a newline argument")
185237
mode = ''.join(sorted(c for c in mode if c not in 'bt'))
186-
if text:
187-
try:
188-
attr = getattr(cls, f'__open_{mode}__')
189-
except AttributeError:
190-
pass
191-
else:
192-
return attr(path, buffering, encoding, errors, newline)
193-
elif encoding is not None:
194-
raise ValueError("binary mode doesn't take an encoding argument")
195-
elif errors is not None:
196-
raise ValueError("binary mode doesn't take an errors argument")
197-
elif newline is not None:
198-
raise ValueError("binary mode doesn't take a newline argument")
199-
200-
try:
201-
attr = getattr(cls, f'__open_{mode}b__')
202-
except AttributeError:
203-
pass
238+
if mode == 'r':
239+
stream = _open_reader(obj)
240+
elif mode in ('a', 'w', 'x'):
241+
stream = _open_writer(obj, mode)
242+
elif mode in ('+r', '+w'):
243+
stream = _open_updater(obj, mode[1])
204244
else:
205-
stream = attr(path, buffering)
206-
if text:
207-
stream = TextIOWrapper(stream, encoding, errors, newline)
208-
return stream
209-
210-
raise TypeError(f"{cls.__name__} can't be opened with mode {mode!r}")
245+
raise ValueError(f'invalid mode: {mode}')
246+
if text:
247+
stream = TextIOWrapper(stream, encoding, errors, newline)
248+
return stream
211249

212250

213251
def vfspath(obj):

Lib/pathlib/types.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from abc import ABC, abstractmethod
1414
from glob import _GlobberBase
1515
from io import text_encoding
16-
from pathlib._os import (magic_open, vfspath, ensure_distinct_paths,
16+
from pathlib._os import (vfsopen, vfspath, ensure_distinct_paths,
1717
ensure_different_files, copyfileobj)
1818
from pathlib import PurePath, Path
1919
from typing import Optional, Protocol, runtime_checkable
@@ -264,18 +264,18 @@ def info(self):
264264
raise NotImplementedError
265265

266266
@abstractmethod
267-
def __open_rb__(self, buffering=-1):
267+
def __open_reader__(self):
268268
"""
269269
Open the file pointed to by this path for reading in binary mode and
270-
return a file object, like open(mode='rb').
270+
return a file object.
271271
"""
272272
raise NotImplementedError
273273

274274
def read_bytes(self):
275275
"""
276276
Open the file in bytes mode, read it, and close the file.
277277
"""
278-
with magic_open(self, mode='rb', buffering=0) as f:
278+
with vfsopen(self, mode='rb') as f:
279279
return f.read()
280280

281281
def read_text(self, encoding=None, errors=None, newline=None):
@@ -285,7 +285,7 @@ def read_text(self, encoding=None, errors=None, newline=None):
285285
# Call io.text_encoding() here to ensure any warning is raised at an
286286
# appropriate stack level.
287287
encoding = text_encoding(encoding)
288-
with magic_open(self, mode='r', encoding=encoding, errors=errors, newline=newline) as f:
288+
with vfsopen(self, mode='r', encoding=encoding, errors=errors, newline=newline) as f:
289289
return f.read()
290290

291291
@abstractmethod
@@ -394,10 +394,10 @@ def mkdir(self):
394394
raise NotImplementedError
395395

396396
@abstractmethod
397-
def __open_wb__(self, buffering=-1):
397+
def __open_writer__(self, mode):
398398
"""
399399
Open the file pointed to by this path for writing in binary mode and
400-
return a file object, like open(mode='wb').
400+
return a file object.
401401
"""
402402
raise NotImplementedError
403403

@@ -407,7 +407,7 @@ def write_bytes(self, data):
407407
"""
408408
# type-check for the buffer interface before truncating the file
409409
view = memoryview(data)
410-
with magic_open(self, mode='wb') as f:
410+
with vfsopen(self, mode='wb') as f:
411411
return f.write(view)
412412

413413
def write_text(self, data, encoding=None, errors=None, newline=None):
@@ -420,7 +420,7 @@ def write_text(self, data, encoding=None, errors=None, newline=None):
420420
if not isinstance(data, str):
421421
raise TypeError('data must be str, not %s' %
422422
data.__class__.__name__)
423-
with magic_open(self, mode='w', encoding=encoding, errors=errors, newline=newline) as f:
423+
with vfsopen(self, mode='w', encoding=encoding, errors=errors, newline=newline) as f:
424424
return f.write(data)
425425

426426
def _copy_from(self, source, follow_symlinks=True):
@@ -439,8 +439,8 @@ def _copy_from(self, source, follow_symlinks=True):
439439
stack.append((child, dst.joinpath(child.name)))
440440
else:
441441
ensure_different_files(src, dst)
442-
with magic_open(src, 'rb') as source_f:
443-
with magic_open(dst, 'wb') as target_f:
442+
with vfsopen(src, 'rb') as source_f:
443+
with vfsopen(dst, 'wb') as target_f:
444444
copyfileobj(source_f, target_f)
445445

446446

Lib/test/test_pathlib/support/local_path.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def __init__(self, *pathsegments):
145145
super().__init__(*pathsegments)
146146
self.info = LocalPathInfo(self)
147147

148-
def __open_rb__(self, buffering=-1):
148+
def __open_reader__(self):
149149
return open(self, 'rb')
150150

151151
def iterdir(self):
@@ -163,8 +163,8 @@ class WritableLocalPath(_WritablePath, LexicalPath):
163163
__slots__ = ()
164164
__fspath__ = LexicalPath.__vfspath__
165165

166-
def __open_wb__(self, buffering=-1):
167-
return open(self, 'wb')
166+
def __open_writer__(self, mode):
167+
return open(self, f'{mode}b')
168168

169169
def mkdir(self, mode=0o777):
170170
os.mkdir(self, mode)

Lib/test/test_pathlib/support/zip_path.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -264,13 +264,13 @@ def info(self):
264264
tree = self.zip_file.filelist.tree
265265
return tree.resolve(vfspath(self), follow_symlinks=False)
266266

267-
def __open_rb__(self, buffering=-1):
267+
def __open_reader__(self):
268268
info = self.info.resolve()
269269
if not info.exists():
270270
raise FileNotFoundError(errno.ENOENT, "File not found", self)
271271
elif info.is_dir():
272272
raise IsADirectoryError(errno.EISDIR, "Is a directory", self)
273-
return self.zip_file.open(info.zip_info, 'r')
273+
return self.zip_file.open(info.zip_info)
274274

275275
def iterdir(self):
276276
info = self.info.resolve()
@@ -320,8 +320,8 @@ def __repr__(self):
320320
def with_segments(self, *pathsegments):
321321
return type(self)(*pathsegments, zip_file=self.zip_file)
322322

323-
def __open_wb__(self, buffering=-1):
324-
return self.zip_file.open(vfspath(self), 'w')
323+
def __open_writer__(self, mode):
324+
return self.zip_file.open(vfspath(self), mode)
325325

326326
def mkdir(self, mode=0o777):
327327
zinfo = zipfile.ZipInfo(vfspath(self) + '/')

Lib/test/test_pathlib/test_read.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313

1414
if is_pypi:
1515
from pathlib_abc import PathInfo, _ReadablePath
16-
from pathlib_abc._os import magic_open
16+
from pathlib_abc._os import vfsopen
1717
else:
1818
from pathlib.types import PathInfo, _ReadablePath
19-
from pathlib._os import magic_open
19+
from pathlib._os import vfsopen
2020

2121

2222
class ReadTestBase:
@@ -32,28 +32,34 @@ def test_is_readable(self):
3232

3333
def test_open_r(self):
3434
p = self.root / 'fileA'
35-
with magic_open(p, 'r', encoding='utf-8') as f:
35+
with vfsopen(p, 'r', encoding='utf-8') as f:
3636
self.assertIsInstance(f, io.TextIOBase)
3737
self.assertEqual(f.read(), 'this is file A\n')
3838

39+
def test_open_r_buffering_error(self):
40+
p = self.root / 'fileA'
41+
self.assertRaises(ValueError, vfsopen, p, 'r', buffering=0)
42+
self.assertRaises(ValueError, vfsopen, p, 'r', buffering=1)
43+
self.assertRaises(ValueError, vfsopen, p, 'r', buffering=1024)
44+
3945
@unittest.skipIf(
4046
not getattr(sys.flags, 'warn_default_encoding', 0),
4147
"Requires warn_default_encoding",
4248
)
4349
def test_open_r_encoding_warning(self):
4450
p = self.root / 'fileA'
4551
with self.assertWarns(EncodingWarning) as wc:
46-
with magic_open(p, 'r'):
52+
with vfsopen(p, 'r'):
4753
pass
4854
self.assertEqual(wc.filename, __file__)
4955

5056
def test_open_rb(self):
5157
p = self.root / 'fileA'
52-
with magic_open(p, 'rb') as f:
58+
with vfsopen(p, 'rb') as f:
5359
self.assertEqual(f.read(), b'this is file A\n')
54-
self.assertRaises(ValueError, magic_open, p, 'rb', encoding='utf8')
55-
self.assertRaises(ValueError, magic_open, p, 'rb', errors='strict')
56-
self.assertRaises(ValueError, magic_open, p, 'rb', newline='')
60+
self.assertRaises(ValueError, vfsopen, p, 'rb', encoding='utf8')
61+
self.assertRaises(ValueError, vfsopen, p, 'rb', errors='strict')
62+
self.assertRaises(ValueError, vfsopen, p, 'rb', newline='')
5763

5864
def test_read_bytes(self):
5965
p = self.root / 'fileA'

Lib/test/test_pathlib/test_write.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313

1414
if is_pypi:
1515
from pathlib_abc import _WritablePath
16-
from pathlib_abc._os import magic_open
16+
from pathlib_abc._os import vfsopen
1717
else:
1818
from pathlib.types import _WritablePath
19-
from pathlib._os import magic_open
19+
from pathlib._os import vfsopen
2020

2121

2222
class WriteTestBase:
@@ -31,31 +31,37 @@ def test_is_writable(self):
3131

3232
def test_open_w(self):
3333
p = self.root / 'fileA'
34-
with magic_open(p, 'w', encoding='utf-8') as f:
34+
with vfsopen(p, 'w', encoding='utf-8') as f:
3535
self.assertIsInstance(f, io.TextIOBase)
3636
f.write('this is file A\n')
3737
self.assertEqual(self.ground.readtext(p), 'this is file A\n')
3838

39+
def test_open_w_buffering_error(self):
40+
p = self.root / 'fileA'
41+
self.assertRaises(ValueError, vfsopen, p, 'w', buffering=0)
42+
self.assertRaises(ValueError, vfsopen, p, 'w', buffering=1)
43+
self.assertRaises(ValueError, vfsopen, p, 'w', buffering=1024)
44+
3945
@unittest.skipIf(
4046
not getattr(sys.flags, 'warn_default_encoding', 0),
4147
"Requires warn_default_encoding",
4248
)
4349
def test_open_w_encoding_warning(self):
4450
p = self.root / 'fileA'
4551
with self.assertWarns(EncodingWarning) as wc:
46-
with magic_open(p, 'w'):
52+
with vfsopen(p, 'w'):
4753
pass
4854
self.assertEqual(wc.filename, __file__)
4955

5056
def test_open_wb(self):
5157
p = self.root / 'fileA'
52-
with magic_open(p, 'wb') as f:
58+
with vfsopen(p, 'wb') as f:
5359
#self.assertIsInstance(f, io.BufferedWriter)
5460
f.write(b'this is file A\n')
5561
self.assertEqual(self.ground.readbytes(p), b'this is file A\n')
56-
self.assertRaises(ValueError, magic_open, p, 'wb', encoding='utf8')
57-
self.assertRaises(ValueError, magic_open, p, 'wb', errors='strict')
58-
self.assertRaises(ValueError, magic_open, p, 'wb', newline='')
62+
self.assertRaises(ValueError, vfsopen, p, 'wb', encoding='utf8')
63+
self.assertRaises(ValueError, vfsopen, p, 'wb', errors='strict')
64+
self.assertRaises(ValueError, vfsopen, p, 'wb', newline='')
5965

6066
def test_write_bytes(self):
6167
p = self.root / 'fileA'

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy