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.
This commit is contained in:
Barney Gale 2025-03-12 19:06:43 +00:00 committed by GitHub
parent ea57ffa02e
commit db6a998b18
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 178 additions and 43 deletions

View file

@ -1,5 +1,6 @@
"""
Implementation of ReadablePath for local paths, for use in pathlib tests.
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.
@ -143,3 +144,23 @@ class ReadableLocalPath(pathlib.types._ReadablePath, LexicalPath):
def readlink(self):
return self.with_segments(os.readlink(self))
class WritableLocalPath(pathlib.types._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)

View file

@ -1,5 +1,6 @@
"""
Implementation of ReadablePath for zip file members, for use in pathlib tests.
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.
@ -276,3 +277,48 @@ class ReadableZipPath(pathlib.types._ReadablePath):
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(pathlib.types._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):
self.zip_file.mkdir(str(self), mode)
def symlink_to(self, target, target_is_directory=False):
zinfo = zipfile.ZipInfo(str(self))._for_archive(self.zip_file)
zinfo.external_attr = stat.S_IFLNK << 16
if target_is_directory:
zinfo.external_attr |= 0x10
self.zip_file.writestr(zinfo, str(target))

View file

@ -336,10 +336,6 @@ class ReadablePathTest(JoinablePathTest):
class WritablePathTest(JoinablePathTest):
cls = DummyWritablePath
def test_is_writable(self):
p = self.cls(self.base)
self.assertIsInstance(p, _WritablePath)
class DummyRWPath(DummyWritablePath, DummyReadablePath):
__slots__ = ()
@ -349,43 +345,6 @@ class RWPathTest(WritablePathTest, ReadablePathTest):
cls = DummyRWPath
can_symlink = False
def test_read_write_bytes(self):
p = self.cls(self.base)
(p / 'fileA').write_bytes(b'abcdefg')
self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg')
# Check that trying to write str does not truncate the file.
self.assertRaises(TypeError, (p / 'fileA').write_bytes, 'somestr')
self.assertEqual((p / 'fileA').read_bytes(), b'abcdefg')
def test_read_write_text(self):
p = self.cls(self.base)
(p / 'fileA').write_text('äbcdefg', encoding='latin-1')
self.assertEqual((p / 'fileA').read_text(
encoding='utf-8', errors='ignore'), 'bcdefg')
# Check that trying to write bytes does not truncate the file.
self.assertRaises(TypeError, (p / 'fileA').write_text, b'somebytes')
self.assertEqual((p / 'fileA').read_text(encoding='latin-1'), 'äbcdefg')
def test_write_text_with_newlines(self):
p = self.cls(self.base)
# Check that `\n` character change nothing
(p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\n')
self.assertEqual((p / 'fileA').read_bytes(),
b'abcde\r\nfghlk\n\rmnopq')
# Check that `\r` character replaces `\n`
(p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\r')
self.assertEqual((p / 'fileA').read_bytes(),
b'abcde\r\rfghlk\r\rmnopq')
# Check that `\r\n` character replaces `\n`
(p / 'fileA').write_text('abcde\r\nfghlk\n\rmnopq', newline='\r\n')
self.assertEqual((p / 'fileA').read_bytes(),
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 / 'fileA').write_text('abcde\nfghlk\n\rmnopq')
self.assertEqual((p / 'fileA').read_bytes(),
b'abcde' + os_linesep_byte + b'fghlk' + os_linesep_byte + b'\rmnopq')
def test_copy_file(self):
base = self.cls(self.base)
source = base / 'fileA'

View file

@ -0,0 +1,109 @@
"""
Tests for pathlib.types._WritablePath
"""
import io
import os
import unittest
from pathlib import Path
from pathlib.types import _WritablePath
from pathlib._os import magic_open
from test.test_pathlib.support.local_path import WritableLocalPath, LocalPathGround
from test.test_pathlib.support.zip_path import WritableZipPath, ZipPathGround
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') 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')
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')
def test_write_bytes(self):
p = self.root / 'fileA'
p.write_bytes(b'abcdefg')
self.assertEqual(self.ground.readbytes(p), b'abcdefg')
# Check that trying to write str does not truncate the file.
self.assertRaises(TypeError, p.write_bytes, 'somestr')
self.assertEqual(self.ground.readbytes(p), b'abcdefg')
def test_write_text(self):
p = self.root / 'fileA'
p.write_text('äbcdefg', 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')
self.assertEqual(self.ground.readbytes(p), b'\xe4bcdefg')
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', 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', 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', 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')
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)
class PathWriteTest(WriteTestBase, unittest.TestCase):
ground = LocalPathGround(Path)
if __name__ == "__main__":
unittest.main()