Skip to content

GH-130614: pathlib ABCs: revise test suite for writable paths #131112

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion Lib/test/test_pathlib/support/local_path.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -143,3 +144,23 @@ def iterdir(self):

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)
48 changes: 47 additions & 1 deletion Lib/test/test_pathlib/support/zip_path.py
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -276,3 +277,48 @@ def readlink(self):
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))
41 changes: 0 additions & 41 deletions Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -336,10 +336,6 @@ def test_glob_windows(self):
class WritablePathTest(JoinablePathTest):
cls = DummyWritablePath

def test_is_writable(self):
p = self.cls(self.base)
self.assertIsInstance(p, _WritablePath)


class DummyRWPath(DummyWritablePath, DummyReadablePath):
__slots__ = ()
Expand All @@ -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'
Expand Down
109 changes: 109 additions & 0 deletions Lib/test/test_pathlib/test_write.py
Original file line number Diff line number Diff line change
@@ -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()
Loading