GH-128520: pathlib ABCs: allow tests to be run externally (#131315)

Adjust the tests for the `pathlib.types` module so that they can be run
against the `pathlib-abc` PyPI package, which is a backport of the module
for older Python versions.

Specifically, we add a `.support.is_pypi` switch that is false in the
stdlib and true in the pathlib-abc package. This controls which package
we import, and whether or not we run tests against `PurePath` and `Path`.

For compatibility with older Python versions, we stop using
`zipfile.ZipFile.mkdir()` and `zipfile.ZipInfo._for_archive()`.
This commit is contained in:
Barney Gale 2025-03-21 22:18:20 +00:00 committed by GitHub
parent 56d0f9af14
commit cf9d1a4b6b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 127 additions and 72 deletions

View file

@ -0,0 +1,2 @@
# Set to 'True' if the tests are run against the pathlib-abc PyPI package.
is_pypi = False

View file

@ -4,11 +4,17 @@ Simple implementation of JoinablePath, for use in pathlib tests.
import ntpath
import os.path
import pathlib.types
import posixpath
from . import is_pypi
class LexicalPath(pathlib.types._JoinablePath):
if is_pypi:
from pathlib_abc import _JoinablePath
else:
from pathlib.types import _JoinablePath
class LexicalPath(_JoinablePath):
__slots__ = ('_segments',)
parser = os.path

View file

@ -7,25 +7,36 @@ about local paths in tests.
"""
import os
import pathlib.types
from test.support import os_helper
from test.test_pathlib.support.lexical_path import LexicalPath
from . import is_pypi
from .lexical_path import LexicalPath
if is_pypi:
from shutil import rmtree
from pathlib_abc import PathInfo, _ReadablePath, _WritablePath
can_symlink = True
testfn = "TESTFN"
else:
from pathlib.types import PathInfo, _ReadablePath, _WritablePath
from test.support import os_helper
can_symlink = os_helper.can_symlink()
testfn = os_helper.TESTFN
rmtree = os_helper.rmtree
class LocalPathGround:
can_symlink = os_helper.can_symlink()
can_symlink = can_symlink
def __init__(self, path_cls):
self.path_cls = path_cls
def setup(self, local_suffix=""):
root = self.path_cls(os_helper.TESTFN + local_suffix)
root = self.path_cls(testfn + local_suffix)
os.mkdir(root)
return root
def teardown(self, root):
os_helper.rmtree(root)
rmtree(root)
def create_file(self, p, data=b''):
with open(p, 'wb') as f:
@ -79,7 +90,7 @@ class LocalPathGround:
return f.read()
class LocalPathInfo(pathlib.types.PathInfo):
class LocalPathInfo(PathInfo):
"""
Simple implementation of PathInfo for a local path
"""
@ -123,7 +134,7 @@ class LocalPathInfo(pathlib.types.PathInfo):
return self._is_symlink
class ReadableLocalPath(pathlib.types._ReadablePath, LexicalPath):
class ReadableLocalPath(_ReadablePath, LexicalPath):
"""
Simple implementation of a ReadablePath class for local filesystem paths.
"""
@ -146,7 +157,7 @@ class ReadableLocalPath(pathlib.types._ReadablePath, LexicalPath):
return self.with_segments(os.readlink(self))
class WritableLocalPath(pathlib.types._WritablePath, LexicalPath):
class WritableLocalPath(_WritablePath, LexicalPath):
"""
Simple implementation of a WritablePath class for local filesystem paths.
"""

View file

@ -8,12 +8,18 @@ about zip file members in tests.
import errno
import io
import pathlib.types
import posixpath
import stat
import zipfile
from stat import S_IFMT, S_ISDIR, S_ISREG, S_ISLNK
from . import is_pypi
if is_pypi:
from pathlib_abc import PathInfo, _ReadablePath, _WritablePath
else:
from pathlib.types import PathInfo, _ReadablePath, _WritablePath
class ZipPathGround:
can_symlink = True
@ -31,7 +37,10 @@ class ZipPathGround:
path.zip_file.writestr(str(path), data)
def create_dir(self, path):
path.zip_file.mkdir(str(path))
zip_info = zipfile.ZipInfo(str(path) + '/')
zip_info.external_attr |= stat.S_IFDIR << 16
zip_info.external_attr |= stat.FILE_ATTRIBUTE_DIRECTORY
path.zip_file.writestr(zip_info, '')
def create_symlink(self, path, target):
zip_info = zipfile.ZipInfo(str(path))
@ -80,7 +89,7 @@ class ZipPathGround:
return stat.S_ISLNK(info.external_attr >> 16)
class MissingZipPathInfo:
class MissingZipPathInfo(PathInfo):
"""
PathInfo implementation that is used when a zip file member is missing.
"""
@ -105,7 +114,7 @@ class MissingZipPathInfo:
missing_zip_path_info = MissingZipPathInfo()
class ZipPathInfo:
class ZipPathInfo(PathInfo):
"""
PathInfo implementation for an existing zip file member.
"""
@ -216,7 +225,7 @@ class ZipFileList:
self.tree.resolve(item.filename, create=True).zip_info = item
class ReadableZipPath(pathlib.types._ReadablePath):
class ReadableZipPath(_ReadablePath):
"""
Simple implementation of a ReadablePath class for .zip files.
"""
@ -279,7 +288,7 @@ class ReadableZipPath(pathlib.types._ReadablePath):
return self.with_segments(self.zip_file.read(info.zip_info).decode())
class WritableZipPath(pathlib.types._WritablePath):
class WritableZipPath(_WritablePath):
"""
Simple implementation of a WritablePath class for .zip files.
"""
@ -314,10 +323,13 @@ class WritableZipPath(pathlib.types._WritablePath):
return self.zip_file.open(str(self), 'w')
def mkdir(self, mode=0o777):
self.zip_file.mkdir(str(self), mode)
zinfo = zipfile.ZipInfo(str(self) + '/')
zinfo.external_attr |= stat.S_IFDIR << 16
zinfo.external_attr |= stat.FILE_ATTRIBUTE_DIRECTORY
self.zip_file.writestr(zinfo, '')
def symlink_to(self, target, target_is_directory=False):
zinfo = zipfile.ZipInfo(str(self))._for_archive(self.zip_file)
zinfo = zipfile.ZipInfo(str(self))
zinfo.external_attr = stat.S_IFLNK << 16
if target_is_directory:
zinfo.external_attr |= 0x10

View file

@ -5,10 +5,9 @@ Tests for copying from pathlib.types._ReadablePath to _WritablePath.
import contextlib
import unittest
from pathlib import Path
from test.test_pathlib.support.local_path import LocalPathGround, WritableLocalPath
from test.test_pathlib.support.zip_path import ZipPathGround, ReadableZipPath, WritableZipPath
from .support import is_pypi
from .support.local_path import LocalPathGround
from .support.zip_path import ZipPathGround, ReadableZipPath, WritableZipPath
class CopyTestBase:
@ -53,7 +52,7 @@ class CopyTestBase:
self.target_ground.readbytes(result))
def test_copy_file_to_directory(self):
if not isinstance(self.target_root, WritableLocalPath):
if isinstance(self.target_root, WritableZipPath):
self.skipTest('needs local target')
source = self.source_root / 'fileA'
target = self.target_root / 'copyA'
@ -113,7 +112,7 @@ class CopyTestBase:
self.assertEqual(self.target_ground.readlink(target / 'linkD'), 'dirD')
def test_copy_dir_to_existing_directory(self):
if not isinstance(self.target_root, WritableLocalPath):
if isinstance(self.target_root, WritableZipPath):
self.skipTest('needs local target')
source = self.source_root / 'dirC'
target = self.target_root / 'copyC'
@ -153,19 +152,22 @@ class ZipToZipPathCopyTest(CopyTestBase, unittest.TestCase):
target_ground = ZipPathGround(WritableZipPath)
class ZipToLocalPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = ZipPathGround(ReadableZipPath)
target_ground = LocalPathGround(Path)
if not is_pypi:
from pathlib import Path
class ZipToLocalPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = ZipPathGround(ReadableZipPath)
target_ground = LocalPathGround(Path)
class LocalToZipPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = LocalPathGround(Path)
target_ground = ZipPathGround(WritableZipPath)
class LocalToZipPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = LocalPathGround(Path)
target_ground = ZipPathGround(WritableZipPath)
class LocalToLocalPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = LocalPathGround(Path)
target_ground = LocalPathGround(Path)
class LocalToLocalPathCopyTest(CopyTestBase, unittest.TestCase):
source_ground = LocalPathGround(Path)
target_ground = LocalPathGround(Path)
if __name__ == "__main__":

View file

@ -4,9 +4,13 @@ Tests for pathlib.types._JoinablePath
import unittest
from pathlib import PurePath, Path
from pathlib.types import _PathParser, _JoinablePath
from test.test_pathlib.support.lexical_path import LexicalPath
from .support import is_pypi
from .support.lexical_path import LexicalPath
if is_pypi:
from pathlib_abc import _PathParser, _JoinablePath
else:
from pathlib.types import _PathParser, _JoinablePath
class JoinTestBase:
@ -355,12 +359,14 @@ class LexicalPathJoinTest(JoinTestBase, unittest.TestCase):
cls = LexicalPath
class PurePathJoinTest(JoinTestBase, unittest.TestCase):
cls = PurePath
if not is_pypi:
from pathlib import PurePath, Path
class PurePathJoinTest(JoinTestBase, unittest.TestCase):
cls = PurePath
class PathJoinTest(JoinTestBase, unittest.TestCase):
cls = Path
class PathJoinTest(JoinTestBase, unittest.TestCase):
cls = Path
if __name__ == "__main__":

View file

@ -5,8 +5,8 @@ Tests for Posix-flavoured pathlib.types._JoinablePath
import os
import unittest
from pathlib import PurePosixPath, PosixPath
from test.test_pathlib.support.lexical_path import LexicalPosixPath
from .support import is_pypi
from .support.lexical_path import LexicalPosixPath
class JoinTestBase:
@ -36,13 +36,15 @@ class LexicalPosixPathJoinTest(JoinTestBase, unittest.TestCase):
cls = LexicalPosixPath
class PurePosixPathJoinTest(JoinTestBase, unittest.TestCase):
cls = PurePosixPath
if not is_pypi:
from pathlib import PurePosixPath, PosixPath
class PurePosixPathJoinTest(JoinTestBase, unittest.TestCase):
cls = PurePosixPath
if os.name != 'nt':
class PosixPathJoinTest(JoinTestBase, unittest.TestCase):
cls = PosixPath
if os.name != 'nt':
class PosixPathJoinTest(JoinTestBase, unittest.TestCase):
cls = PosixPath
if __name__ == "__main__":

View file

@ -5,8 +5,8 @@ Tests for Windows-flavoured pathlib.types._JoinablePath
import os
import unittest
from pathlib import PureWindowsPath, WindowsPath
from test.test_pathlib.support.lexical_path import LexicalWindowsPath
from .support import is_pypi
from .support.lexical_path import LexicalWindowsPath
class JoinTestBase:
@ -40,8 +40,6 @@ class JoinTestBase:
pp = p.joinpath('E:d:s')
self.assertEqual(pp, P('E:d:s'))
# Joining onto a UNC path with no root
pp = P('//').joinpath('server')
self.assertEqual(pp, P('//server'))
pp = P('//server').joinpath('share')
self.assertEqual(pp, P(r'//server\share'))
pp = P('//./BootPartition').joinpath('Windows')
@ -54,7 +52,7 @@ class JoinTestBase:
self.assertEqual(p / 'x/y', P(r'C:/a/b\x/y'))
self.assertEqual(p / 'x' / 'y', P(r'C:/a/b\x\y'))
self.assertEqual(p / '/x/y', P('C:/x/y'))
self.assertEqual(p / '/x' / 'y', P('C:/x\y'))
self.assertEqual(p / '/x' / 'y', P(r'C:/x\y'))
# Joining with a different drive => the first path is ignored, even
# if the second path is relative.
self.assertEqual(p / 'D:x/y', P('D:x/y'))
@ -277,13 +275,15 @@ class LexicalWindowsPathJoinTest(JoinTestBase, unittest.TestCase):
cls = LexicalWindowsPath
class PureWindowsPathJoinTest(JoinTestBase, unittest.TestCase):
cls = PureWindowsPath
if not is_pypi:
from pathlib import PureWindowsPath, WindowsPath
class PureWindowsPathJoinTest(JoinTestBase, unittest.TestCase):
cls = PureWindowsPath
if os.name == 'nt':
class WindowsPathJoinTest(JoinTestBase, unittest.TestCase):
cls = WindowsPath
if os.name == 'nt':
class WindowsPathJoinTest(JoinTestBase, unittest.TestCase):
cls = WindowsPath
if __name__ == "__main__":

View file

@ -6,12 +6,16 @@ import collections.abc
import io
import unittest
from pathlib import Path
from pathlib.types import PathInfo, _ReadablePath
from pathlib._os import magic_open
from .support import is_pypi
from .support.local_path import ReadableLocalPath, LocalPathGround
from .support.zip_path import ReadableZipPath, ZipPathGround
from test.test_pathlib.support.local_path import ReadableLocalPath, LocalPathGround
from test.test_pathlib.support.zip_path import ReadableZipPath, ZipPathGround
if is_pypi:
from pathlib_abc import PathInfo, _ReadablePath
from pathlib_abc._os import magic_open
else:
from pathlib.types import PathInfo, _ReadablePath
from pathlib._os import magic_open
class ReadTestBase:
@ -301,8 +305,11 @@ class LocalPathReadTest(ReadTestBase, unittest.TestCase):
ground = LocalPathGround(ReadableLocalPath)
class PathReadTest(ReadTestBase, unittest.TestCase):
ground = LocalPathGround(Path)
if not is_pypi:
from pathlib import Path
class PathReadTest(ReadTestBase, unittest.TestCase):
ground = LocalPathGround(Path)
if __name__ == "__main__":

View file

@ -6,12 +6,16 @@ import io
import os
import unittest
from pathlib import Path
from pathlib.types import _WritablePath
from pathlib._os import magic_open
from .support import is_pypi
from .support.local_path import WritableLocalPath, LocalPathGround
from .support.zip_path import WritableZipPath, ZipPathGround
from test.test_pathlib.support.local_path import WritableLocalPath, LocalPathGround
from test.test_pathlib.support.zip_path import WritableZipPath, ZipPathGround
if is_pypi:
from pathlib_abc import _WritablePath
from pathlib_abc._os import magic_open
else:
from pathlib.types import _WritablePath
from pathlib._os import magic_open
class WriteTestBase:
@ -101,8 +105,11 @@ class LocalPathWriteTest(WriteTestBase, unittest.TestCase):
ground = LocalPathGround(WritableLocalPath)
class PathWriteTest(WriteTestBase, unittest.TestCase):
ground = LocalPathGround(Path)
if not is_pypi:
from pathlib import Path
class PathWriteTest(WriteTestBase, unittest.TestCase):
ground = LocalPathGround(Path)
if __name__ == "__main__":