Skip to content
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

GH-110109: pathlib ABCs: do not vary path syntax by host OS. #113219

Merged
merged 7 commits into from
Dec 22, 2023
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
1 change: 1 addition & 0 deletions Lib/pathlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class PurePath(_abc.PurePathBase):
# path. It's set when `__hash__()` is called for the first time.
'_hash',
)
pathmod = os.path

def __new__(cls, *args, **kwargs):
"""Construct a PurePath from one or several strings and or existing
Expand Down
3 changes: 1 addition & 2 deletions Lib/pathlib/_abc.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import functools
import io
import ntpath
import os
import posixpath
import sys
import warnings
Expand Down Expand Up @@ -204,7 +203,7 @@ class PurePathBase:
# work from occurring when `resolve()` calls `stat()` or `readlink()`.
'_resolving',
)
pathmod = os.path
pathmod = posixpath

def __init__(self, *paths):
self._raw_paths = paths
Expand Down
59 changes: 59 additions & 0 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
import os
import sys
import errno
import ntpath
import pathlib
import pickle
import posixpath
import socket
import stat
import tempfile
Expand Down Expand Up @@ -39,6 +41,50 @@
class PurePathTest(test_pathlib_abc.DummyPurePathTest):
cls = pathlib.PurePath

# Make sure any symbolic links in the base test path are resolved.
base = os.path.realpath(TESTFN)

def test_concrete_class(self):
if self.cls is pathlib.PurePath:
expected = pathlib.PureWindowsPath if os.name == 'nt' else pathlib.PurePosixPath
else:
expected = self.cls
p = self.cls('a')
self.assertIs(type(p), expected)

def test_concrete_pathmod(self):
if self.cls is pathlib.PurePosixPath:
expected = posixpath
elif self.cls is pathlib.PureWindowsPath:
expected = ntpath
else:
expected = os.path
p = self.cls('a')
self.assertIs(p.pathmod, expected)

def test_different_pathmods_unequal(self):
p = self.cls('a')
if p.pathmod is posixpath:
q = pathlib.PureWindowsPath('a')
else:
q = pathlib.PurePosixPath('a')
self.assertNotEqual(p, q)

def test_different_pathmods_unordered(self):
p = self.cls('a')
if p.pathmod is posixpath:
q = pathlib.PureWindowsPath('a')
else:
q = pathlib.PurePosixPath('a')
with self.assertRaises(TypeError):
p < q
with self.assertRaises(TypeError):
p <= q
with self.assertRaises(TypeError):
p > q
with self.assertRaises(TypeError):
p >= q

def test_constructor_nested(self):
P = self.cls
P(FakePath("a/b/c"))
Expand Down Expand Up @@ -958,6 +1004,19 @@ def tempdir(self):
self.addCleanup(os_helper.rmtree, d)
return d

def test_matches_pathbase_api(self):
our_names = {name for name in dir(self.cls) if name[0] != '_'}
path_names = {name for name in dir(pathlib._abc.PathBase) if name[0] != '_'}
self.assertEqual(our_names, path_names)
for attr_name in our_names:
if attr_name == 'pathmod':
# On Windows, Path.pathmod is ntpath, but PathBase.pathmod is
# posixpath, and so their docstrings differ.
continue
our_attr = getattr(self.cls, attr_name)
path_attr = getattr(pathlib._abc.PathBase, attr_name)
self.assertEqual(our_attr.__doc__, path_attr.__doc__)

def test_concrete_class(self):
if self.cls is pathlib.Path:
expected = pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath
Expand Down
84 changes: 26 additions & 58 deletions Lib/test/test_pathlib/test_pathlib_abc.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ def test_magic_methods(self):
self.assertIs(P.__gt__, object.__gt__)
self.assertIs(P.__ge__, object.__ge__)

def test_pathmod(self):
self.assertIs(self.cls.pathmod, posixpath)


class DummyPurePath(pathlib._abc.PurePathBase):
def __eq__(self, other):
Expand All @@ -52,8 +55,8 @@ def __hash__(self):
class DummyPurePathTest(unittest.TestCase):
cls = DummyPurePath

# Make sure any symbolic links in the base test path are resolved.
base = os.path.realpath(TESTFN)
# Use a base path that's unrelated to any real filesystem path.
base = f'/this/path/kills/fascists/{TESTFN}'

# Keys are canonical paths, values are list of tuples of arguments
# supposed to produce equal paths.
Expand Down Expand Up @@ -86,37 +89,6 @@ def test_constructor_common(self):
P('a/b/c')
P('/a/b/c')

def test_concrete_class(self):
if self.cls is pathlib.PurePath:
expected = pathlib.PureWindowsPath if os.name == 'nt' else pathlib.PurePosixPath
else:
expected = self.cls
p = self.cls('a')
self.assertIs(type(p), expected)

def test_different_pathmods_unequal(self):
p = self.cls('a')
if p.pathmod is posixpath:
q = pathlib.PureWindowsPath('a')
else:
q = pathlib.PurePosixPath('a')
self.assertNotEqual(p, q)

def test_different_pathmods_unordered(self):
p = self.cls('a')
if p.pathmod is posixpath:
q = pathlib.PureWindowsPath('a')
else:
q = pathlib.PurePosixPath('a')
with self.assertRaises(TypeError):
p < q
with self.assertRaises(TypeError):
p <= q
with self.assertRaises(TypeError):
p > q
with self.assertRaises(TypeError):
p >= q

def _check_str_subclass(self, *args):
# Issue #21127: it should be possible to construct a PurePath object
# from a str subclass instance, and it then gets converted to
Expand Down Expand Up @@ -721,15 +693,6 @@ def test_fspath_common(self):
def test_as_bytes_common(self):
self.assertRaises(TypeError, bytes, self.cls())

def test_matches_path_api(self):
our_names = {name for name in dir(self.cls) if name[0] != '_'}
path_names = {name for name in dir(pathlib.Path) if name[0] != '_'}
self.assertEqual(our_names, path_names)
for attr_name in our_names:
our_attr = getattr(self.cls, attr_name)
path_attr = getattr(pathlib.Path, attr_name)
self.assertEqual(our_attr.__doc__, path_attr.__doc__)


class DummyPathIO(io.BytesIO):
"""
Expand Down Expand Up @@ -905,11 +868,13 @@ def assertFileNotFound(self, func, *args, **kwargs):
self.assertEqual(cm.exception.errno, errno.ENOENT)

def assertEqualNormCase(self, path_a, path_b):
self.assertEqual(os.path.normcase(path_a), os.path.normcase(path_b))
normcase = self.pathmod.normcase
self.assertEqual(normcase(path_a), normcase(path_b))

def test_samefile(self):
fileA_path = os.path.join(self.base, 'fileA')
fileB_path = os.path.join(self.base, 'dirB', 'fileB')
pathmod = self.pathmod
fileA_path = pathmod.join(self.base, 'fileA')
fileB_path = pathmod.join(self.base, 'dirB', 'fileB')
p = self.cls(fileA_path)
pp = self.cls(fileA_path)
q = self.cls(fileB_path)
Expand All @@ -918,7 +883,7 @@ def test_samefile(self):
self.assertFalse(p.samefile(fileB_path))
self.assertFalse(p.samefile(q))
# Test the non-existent file case
non_existent = os.path.join(self.base, 'foo')
non_existent = pathmod.join(self.base, 'foo')
r = self.cls(non_existent)
self.assertRaises(FileNotFoundError, p.samefile, r)
self.assertRaises(FileNotFoundError, p.samefile, non_existent)
Expand Down Expand Up @@ -1379,14 +1344,15 @@ def test_resolve_common(self):
p.resolve(strict=True)
self.assertEqual(cm.exception.errno, errno.ENOENT)
# Non-strict
pathmod = self.pathmod
self.assertEqualNormCase(str(p.resolve(strict=False)),
os.path.join(self.base, 'foo'))
pathmod.join(self.base, 'foo'))
p = P(self.base, 'foo', 'in', 'spam')
self.assertEqualNormCase(str(p.resolve(strict=False)),
os.path.join(self.base, 'foo', 'in', 'spam'))
pathmod.join(self.base, 'foo', 'in', 'spam'))
p = P(self.base, '..', 'foo', 'in', 'spam')
self.assertEqualNormCase(str(p.resolve(strict=False)),
os.path.abspath(os.path.join('foo', 'in', 'spam')))
pathmod.join(pathmod.dirname(self.base), 'foo', 'in', 'spam'))
# These are all relative symlinks.
p = P(self.base, 'dirB', 'fileB')
self._check_resolve_relative(p, p)
Expand All @@ -1401,7 +1367,7 @@ def test_resolve_common(self):
self._check_resolve_relative(p, P(self.base, 'dirB', 'fileB', 'foo', 'in',
'spam'), False)
p = P(self.base, 'dirA', 'linkC', '..', 'foo', 'in', 'spam')
if os.name == 'nt' and isinstance(p, pathlib.Path):
if self.cls.pathmod is not posixpath:
# In Windows, if linkY points to dirB, 'dirA\linkY\..'
# resolves to 'dirA' without resolving linkY first.
self._check_resolve_relative(p, P(self.base, 'dirA', 'foo', 'in',
Expand All @@ -1421,7 +1387,7 @@ def test_resolve_common(self):
self._check_resolve_relative(p, P(self.base, 'dirB', 'foo', 'in', 'spam'),
False)
p = P(self.base, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam')
if os.name == 'nt' and isinstance(p, pathlib.Path):
if self.cls.pathmod is not posixpath:
# In Windows, if linkY points to dirB, 'dirA\linkY\..'
# resolves to 'dirA' without resolving linkY first.
self._check_resolve_relative(p, P(d, 'foo', 'in', 'spam'), False)
Expand All @@ -1434,10 +1400,11 @@ def test_resolve_dot(self):
# See http://web.archive.org/web/20200623062557/https://bitbucket.org/pitrou/pathlib/issues/9/
if not self.can_symlink:
self.skipTest("symlinks required")
pathmod = self.pathmod
p = self.cls(self.base)
p.joinpath('0').symlink_to('.', target_is_directory=True)
p.joinpath('1').symlink_to(os.path.join('0', '0'), target_is_directory=True)
p.joinpath('2').symlink_to(os.path.join('1', '1'), target_is_directory=True)
p.joinpath('1').symlink_to(pathmod.join('0', '0'), target_is_directory=True)
p.joinpath('2').symlink_to(pathmod.join('1', '1'), target_is_directory=True)
q = p / '2'
self.assertEqual(q.resolve(strict=True), p)
r = q / '3' / '4'
Expand All @@ -1454,7 +1421,7 @@ def _check_symlink_loop(self, *args):
def test_resolve_loop(self):
if not self.can_symlink:
self.skipTest("symlinks required")
if os.name == 'nt' and issubclass(self.cls, pathlib.Path):
if self.cls.pathmod is not posixpath:
self.skipTest("symlink loops work differently with concrete Windows paths")
# Loops with relative symlinks.
self.cls(self.base, 'linkX').symlink_to('linkX/inside')
Expand Down Expand Up @@ -1657,10 +1624,11 @@ def _check_complex_symlinks(self, link0_target):
self.skipTest("symlinks required")

# Test solving a non-looping chain of symlinks (issue #19887).
pathmod = self.pathmod
P = self.cls(self.base)
P.joinpath('link1').symlink_to(os.path.join('link0', 'link0'), target_is_directory=True)
P.joinpath('link2').symlink_to(os.path.join('link1', 'link1'), target_is_directory=True)
P.joinpath('link3').symlink_to(os.path.join('link2', 'link2'), target_is_directory=True)
P.joinpath('link1').symlink_to(pathmod.join('link0', 'link0'), target_is_directory=True)
P.joinpath('link2').symlink_to(pathmod.join('link1', 'link1'), target_is_directory=True)
P.joinpath('link3').symlink_to(pathmod.join('link2', 'link2'), target_is_directory=True)
P.joinpath('link0').symlink_to(link0_target, target_is_directory=True)

# Resolve absolute paths.
Expand Down Expand Up @@ -1707,7 +1675,7 @@ def test_complex_symlinks_relative(self):
self._check_complex_symlinks('.')

def test_complex_symlinks_relative_dot_dot(self):
self._check_complex_symlinks(os.path.join('dirA', '..'))
self._check_complex_symlinks(self.pathmod.join('dirA', '..'))

def setUpWalk(self):
# Build:
Expand Down
Loading