Skip to content

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

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 3 commits into from
Mar 21, 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
2 changes: 2 additions & 0 deletions Lib/test/test_pathlib/support/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Set to 'True' if the tests are run against the pathlib-abc PyPI package.
is_pypi = False
10 changes: 8 additions & 2 deletions Lib/test/test_pathlib/support/lexical_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,17 @@

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

Expand Down
29 changes: 20 additions & 9 deletions Lib/test/test_pathlib/support/local_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,36 @@
"""

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:
Expand Down Expand Up @@ -79,7 +90,7 @@ def readbytes(self, p):
return f.read()


class LocalPathInfo(pathlib.types.PathInfo):
class LocalPathInfo(PathInfo):
"""
Simple implementation of PathInfo for a local path
"""
Expand Down Expand Up @@ -123,7 +134,7 @@ def is_symlink(self):
return self._is_symlink


class ReadableLocalPath(pathlib.types._ReadablePath, LexicalPath):
class ReadableLocalPath(_ReadablePath, LexicalPath):
"""
Simple implementation of a ReadablePath class for local filesystem paths.
"""
Expand All @@ -146,7 +157,7 @@ def 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.
"""
Expand Down
28 changes: 20 additions & 8 deletions Lib/test/test_pathlib/support/zip_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@

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
Expand All @@ -31,7 +37,10 @@ def create_file(self, path, data=b''):
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))
Expand Down Expand Up @@ -80,7 +89,7 @@ def islink(self, p):
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.
"""
Expand All @@ -105,7 +114,7 @@ def resolve(self):
missing_zip_path_info = MissingZipPathInfo()


class ZipPathInfo:
class ZipPathInfo(PathInfo):
"""
PathInfo implementation for an existing zip file member.
"""
Expand Down Expand Up @@ -216,7 +225,7 @@ def append(self, 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.
"""
Expand Down Expand Up @@ -279,7 +288,7 @@ def readlink(self):
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.
"""
Expand Down Expand Up @@ -314,10 +323,13 @@ 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)
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
Expand Down
32 changes: 17 additions & 15 deletions Lib/test/test_pathlib/test_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
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:
Expand Down Expand Up @@ -53,7 +52,7 @@ def test_copy_file_to_existing_file(self):
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'
Expand Down Expand Up @@ -113,7 +112,7 @@ def test_copy_dir_follow_symlinks_false(self):
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'
Expand Down Expand Up @@ -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__":
Expand Down
20 changes: 13 additions & 7 deletions Lib/test/test_pathlib/test_join.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

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:
Expand Down Expand Up @@ -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__":
Expand Down
16 changes: 9 additions & 7 deletions Lib/test/test_pathlib/test_join_posix.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
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:
Expand Down Expand Up @@ -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__":
Expand Down
20 changes: 10 additions & 10 deletions Lib/test/test_pathlib/test_join_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
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:
Expand Down Expand Up @@ -40,8 +40,6 @@ def test_join(self):
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')
Expand All @@ -54,7 +52,7 @@ def test_div(self):
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'))
Expand Down Expand Up @@ -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__":
Expand Down
21 changes: 14 additions & 7 deletions Lib/test/test_pathlib/test_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
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:
Expand Down Expand Up @@ -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__":
Expand Down
Loading
Loading