Skip to content

Commit db6a998

Browse files
authored
GH-130614: pathlib ABCs: revise test suite for writable paths (#131112)
Test `pathlib.types._WritablePath` in a dedicated test module. These tests cover `WritableZipPath`, `WritableLocalPath` and `Path`, where the former two classes are implementations of `_WritablePath` for use in tests.
1 parent ea57ffa commit db6a998

File tree

4 files changed

+178
-43
lines changed

4 files changed

+178
-43
lines changed

Lib/test/test_pathlib/support/local_path.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""
2-
Implementation of ReadablePath for local paths, for use in pathlib tests.
2+
Implementations of ReadablePath and WritablePath for local paths, for use in
3+
pathlib tests.
34
45
LocalPathGround is also defined here. It helps establish the "ground truth"
56
about local paths in tests.
@@ -143,3 +144,23 @@ def iterdir(self):
143144

144145
def readlink(self):
145146
return self.with_segments(os.readlink(self))
147+
148+
149+
class WritableLocalPath(pathlib.types._WritablePath, LexicalPath):
150+
"""
151+
Simple implementation of a WritablePath class for local filesystem paths.
152+
"""
153+
154+
__slots__ = ()
155+
156+
def __fspath__(self):
157+
return str(self)
158+
159+
def __open_wb__(self, buffering=-1):
160+
return open(self, 'wb')
161+
162+
def mkdir(self, mode=0o777):
163+
os.mkdir(self, mode)
164+
165+
def symlink_to(self, target, target_is_directory=False):
166+
os.symlink(target, self, target_is_directory)

Lib/test/test_pathlib/support/zip_path.py

+47-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""
2-
Implementation of ReadablePath for zip file members, for use in pathlib tests.
2+
Implementations of ReadablePath and WritablePath for zip file members, for use
3+
in pathlib tests.
34
45
ZipPathGround is also defined here. It helps establish the "ground truth"
56
about zip file members in tests.
@@ -276,3 +277,48 @@ def readlink(self):
276277
elif not info.is_symlink():
277278
raise OSError(errno.EINVAL, "Not a symlink", self)
278279
return self.with_segments(self.zip_file.read(info.zip_info).decode())
280+
281+
282+
class WritableZipPath(pathlib.types._WritablePath):
283+
"""
284+
Simple implementation of a WritablePath class for .zip files.
285+
"""
286+
287+
__slots__ = ('_segments', 'zip_file')
288+
parser = posixpath
289+
290+
def __init__(self, *pathsegments, zip_file):
291+
self._segments = pathsegments
292+
self.zip_file = zip_file
293+
294+
def __hash__(self):
295+
return hash((str(self), self.zip_file))
296+
297+
def __eq__(self, other):
298+
if not isinstance(other, WritableZipPath):
299+
return NotImplemented
300+
return str(self) == str(other) and self.zip_file is other.zip_file
301+
302+
def __str__(self):
303+
if not self._segments:
304+
return ''
305+
return self.parser.join(*self._segments)
306+
307+
def __repr__(self):
308+
return f'{type(self).__name__}({str(self)!r}, zip_file={self.zip_file!r})'
309+
310+
def with_segments(self, *pathsegments):
311+
return type(self)(*pathsegments, zip_file=self.zip_file)
312+
313+
def __open_wb__(self, buffering=-1):
314+
return self.zip_file.open(str(self), 'w')
315+
316+
def mkdir(self, mode=0o777):
317+
self.zip_file.mkdir(str(self), mode)
318+
319+
def symlink_to(self, target, target_is_directory=False):
320+
zinfo = zipfile.ZipInfo(str(self))._for_archive(self.zip_file)
321+
zinfo.external_attr = stat.S_IFLNK << 16
322+
if target_is_directory:
323+
zinfo.external_attr |= 0x10
324+
self.zip_file.writestr(zinfo, str(target))

Lib/test/test_pathlib/test_pathlib_abc.py

-41
Original file line numberDiff line numberDiff line change
@@ -336,10 +336,6 @@ def test_glob_windows(self):
336336
class WritablePathTest(JoinablePathTest):
337337
cls = DummyWritablePath
338338

339-
def test_is_writable(self):
340-
p = self.cls(self.base)
341-
self.assertIsInstance(p, _WritablePath)
342-
343339

344340
class DummyRWPath(DummyWritablePath, DummyReadablePath):
345341
__slots__ = ()
@@ -349,43 +345,6 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
349345
cls = DummyRWPath
350346
can_symlink = False
351347

352-
def test_read_write_bytes(self):
353-
p = self.cls(self.base)
354-
(p / 'fileA').write_bytes(b'abcdefg')
355-
self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg')
356-
# Check that trying to write str does not truncate the file.
357-
self.assertRaises(TypeError, (p / 'fileA').write_bytes, 'somestr')
358-
self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg')
359-
360-
def test_read_write_text(self):
361-
p = self.cls(self.base)
362-
(p / 'fileA').write_text('äbcdefg', encoding='latin-1')
363-
self.assertEqual((p / 'fileA').read_text(
364-
encoding='utf-8', errors='ignore'), 'bcdefg')
365-
# Check that trying to write bytes does not truncate the file.
366-
self.assertRaises(TypeError, (p / 'fileA').write_text, b'somebytes')
367-
self.assertEqual((p / 'fileA').read_text(encoding='latin-1'), 'äbcdefg')
368-
369-
def test_write_text_with_newlines(self):
370-
p = self.cls(self.base)
371-
# Check that `\n` character change nothing
372-
(p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\n')
373-
self.assertEqual((p / 'fileA').read_bytes(),
374-
b'abcde\r\nfghlk\n\rmnopq')
375-
# Check that `\r` character replaces `\n`
376-
(p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\r')
377-
self.assertEqual((p / 'fileA').read_bytes(),
378-
b'abcde\r\rfghlk\r\rmnopq')
379-
# Check that `\r\n` character replaces `\n`
380-
(p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\r\n')
381-
self.assertEqual((p / 'fileA').read_bytes(),
382-
b'abcde\r\r\nfghlk\r\n\rmnopq')
383-
# Check that no argument passed will change `\n` to `os.linesep`
384-
os_linesep_byte = bytes(os.linesep, encoding='ascii')
385-
(p / 'fileA').write_text('abcde\nfghlk\n\rmnopq')
386-
self.assertEqual((p / 'fileA').read_bytes(),
387-
b'abcde' + os_linesep_byte + b'fghlk' + os_linesep_byte + b'\rmnopq')
388-
389348
def test_copy_file(self):
390349
base = self.cls(self.base)
391350
source = base / 'fileA'

Lib/test/test_pathlib/test_write.py

+109
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"""
2+
Tests for pathlib.types._WritablePath
3+
"""
4+
5+
import io
6+
import os
7+
import unittest
8+
9+
from pathlib import Path
10+
from pathlib.types import _WritablePath
11+
from pathlib._os import magic_open
12+
13+
from test.test_pathlib.support.local_path import WritableLocalPath, LocalPathGround
14+
from test.test_pathlib.support.zip_path import WritableZipPath, ZipPathGround
15+
16+
17+
class WriteTestBase:
18+
def setUp(self):
19+
self.root = self.ground.setup()
20+
21+
def tearDown(self):
22+
self.ground.teardown(self.root)
23+
24+
def test_is_writable(self):
25+
self.assertIsInstance(self.root, _WritablePath)
26+
27+
def test_open_w(self):
28+
p = self.root / 'fileA'
29+
with magic_open(p, 'w') as f:
30+
self.assertIsInstance(f, io.TextIOBase)
31+
f.write('this is file A\n')
32+
self.assertEqual(self.ground.readtext(p), 'this is file A\n')
33+
34+
def test_open_wb(self):
35+
p = self.root / 'fileA'
36+
with magic_open(p, 'wb') as f:
37+
#self.assertIsInstance(f, io.BufferedWriter)
38+
f.write(b'this is file A\n')
39+
self.assertEqual(self.ground.readbytes(p), b'this is file A\n')
40+
41+
def test_write_bytes(self):
42+
p = self.root / 'fileA'
43+
p.write_bytes(b'abcdefg')
44+
self.assertEqual(self.ground.readbytes(p), b'abcdefg')
45+
# Check that trying to write str does not truncate the file.
46+
self.assertRaises(TypeError, p.write_bytes, 'somestr')
47+
self.assertEqual(self.ground.readbytes(p), b'abcdefg')
48+
49+
def test_write_text(self):
50+
p = self.root / 'fileA'
51+
p.write_text('äbcdefg', encoding='latin-1')
52+
self.assertEqual(self.ground.readbytes(p), b'\xe4bcdefg')
53+
# Check that trying to write bytes does not truncate the file.
54+
self.assertRaises(TypeError, p.write_text, b'somebytes')
55+
self.assertEqual(self.ground.readbytes(p), b'\xe4bcdefg')
56+
57+
def test_write_text_with_newlines(self):
58+
# Check that `\n` character change nothing
59+
p = self.root / 'fileA'
60+
p.write_text('abcde\r\nfghlk\n\rmnopq', newline='\n')
61+
self.assertEqual(self.ground.readbytes(p), b'abcde\r\nfghlk\n\rmnopq')
62+
63+
# Check that `\r` character replaces `\n`
64+
p = self.root / 'fileB'
65+
p.write_text('abcde\r\nfghlk\n\rmnopq', newline='\r')
66+
self.assertEqual(self.ground.readbytes(p), b'abcde\r\rfghlk\r\rmnopq')
67+
68+
# Check that `\r\n` character replaces `\n`
69+
p = self.root / 'fileC'
70+
p.write_text('abcde\r\nfghlk\n\rmnopq', newline='\r\n')
71+
self.assertEqual(self.ground.readbytes(p), b'abcde\r\r\nfghlk\r\n\rmnopq')
72+
73+
# Check that no argument passed will change `\n` to `os.linesep`
74+
os_linesep_byte = bytes(os.linesep, encoding='ascii')
75+
p = self.root / 'fileD'
76+
p.write_text('abcde\nfghlk\n\rmnopq')
77+
self.assertEqual(self.ground.readbytes(p),
78+
b'abcde' + os_linesep_byte +
79+
b'fghlk' + os_linesep_byte + b'\rmnopq')
80+
81+
def test_mkdir(self):
82+
p = self.root / 'newdirA'
83+
self.assertFalse(self.ground.isdir(p))
84+
p.mkdir()
85+
self.assertTrue(self.ground.isdir(p))
86+
87+
def test_symlink_to(self):
88+
if not self.ground.can_symlink:
89+
self.skipTest('needs symlinks')
90+
link = self.root.joinpath('linkA')
91+
link.symlink_to('fileA')
92+
self.assertTrue(self.ground.islink(link))
93+
self.assertEqual(self.ground.readlink(link), 'fileA')
94+
95+
96+
class ZipPathWriteTest(WriteTestBase, unittest.TestCase):
97+
ground = ZipPathGround(WritableZipPath)
98+
99+
100+
class LocalPathWriteTest(WriteTestBase, unittest.TestCase):
101+
ground = LocalPathGround(WritableLocalPath)
102+
103+
104+
class PathWriteTest(WriteTestBase, unittest.TestCase):
105+
ground = LocalPathGround(Path)
106+
107+
108+
if __name__ == "__main__":
109+
unittest.main()

0 commit comments

Comments
 (0)