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 ntpath
import os.path import os.path
import pathlib.types
import posixpath 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',) __slots__ = ('_segments',)
parser = os.path parser = os.path

View file

@ -7,25 +7,36 @@ about local paths in tests.
""" """
import os import os
import pathlib.types
from test.support import os_helper from . import is_pypi
from test.test_pathlib.support.lexical_path import LexicalPath 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: class LocalPathGround:
can_symlink = os_helper.can_symlink() can_symlink = can_symlink
def __init__(self, path_cls): def __init__(self, path_cls):
self.path_cls = path_cls self.path_cls = path_cls
def setup(self, local_suffix=""): def setup(self, local_suffix=""):
root = self.path_cls(os_helper.TESTFN + local_suffix) root = self.path_cls(testfn + local_suffix)
os.mkdir(root) os.mkdir(root)
return root return root
def teardown(self, root): def teardown(self, root):
os_helper.rmtree(root) rmtree(root)
def create_file(self, p, data=b''): def create_file(self, p, data=b''):
with open(p, 'wb') as f: with open(p, 'wb') as f:
@ -79,7 +90,7 @@ class LocalPathGround:
return f.read() return f.read()
class LocalPathInfo(pathlib.types.PathInfo): class LocalPathInfo(PathInfo):
""" """
Simple implementation of PathInfo for a local path Simple implementation of PathInfo for a local path
""" """
@ -123,7 +134,7 @@ class LocalPathInfo(pathlib.types.PathInfo):
return self._is_symlink return self._is_symlink
class ReadableLocalPath(pathlib.types._ReadablePath, LexicalPath): class ReadableLocalPath(_ReadablePath, LexicalPath):
""" """
Simple implementation of a ReadablePath class for local filesystem paths. 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)) 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. 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 errno
import io import io
import pathlib.types
import posixpath import posixpath
import stat import stat
import zipfile import zipfile
from stat import S_IFMT, S_ISDIR, S_ISREG, S_ISLNK 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: class ZipPathGround:
can_symlink = True can_symlink = True
@ -31,7 +37,10 @@ class ZipPathGround:
path.zip_file.writestr(str(path), data) path.zip_file.writestr(str(path), data)
def create_dir(self, path): 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): def create_symlink(self, path, target):
zip_info = zipfile.ZipInfo(str(path)) zip_info = zipfile.ZipInfo(str(path))
@ -80,7 +89,7 @@ class ZipPathGround:
return stat.S_ISLNK(info.external_attr >> 16) 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. PathInfo implementation that is used when a zip file member is missing.
""" """
@ -105,7 +114,7 @@ class MissingZipPathInfo:
missing_zip_path_info = MissingZipPathInfo() missing_zip_path_info = MissingZipPathInfo()
class ZipPathInfo: class ZipPathInfo(PathInfo):
""" """
PathInfo implementation for an existing zip file member. PathInfo implementation for an existing zip file member.
""" """
@ -216,7 +225,7 @@ class ZipFileList:
self.tree.resolve(item.filename, create=True).zip_info = item 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. 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()) 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. 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') return self.zip_file.open(str(self), 'w')
def mkdir(self, mode=0o777): 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): 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 zinfo.external_attr = stat.S_IFLNK << 16
if target_is_directory: if target_is_directory:
zinfo.external_attr |= 0x10 zinfo.external_attr |= 0x10

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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