Skip to content

Commit a0d3d3e

Browse files
authored
GH-110109: pathlib ABCs: do not vary path syntax by host OS. (#113219)
Change the value of `pathlib._abc.PurePathBase.pathmod` from `os.path` to `posixpath`. User subclasses of `PurePathBase` and `PathBase` previously used the host OS's path syntax, e.g. backslashes as separators on Windows. This is wrong in most use cases, and likely to catch developers out unless they test on both Windows and non-Windows machines. In this patch we change the default to POSIX syntax, regardless of OS. This is somewhat arguable (why not make all aspects of syntax abstract and individually configurable?) but an improvement all the same. This change has no effect on `PurePath`, `Path`, nor their subclasses. Only private APIs are affected.
1 parent ff5e131 commit a0d3d3e

File tree

4 files changed

+87
-60
lines changed

4 files changed

+87
-60
lines changed

Lib/pathlib/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ class PurePath(_abc.PurePathBase):
5959
# path. It's set when `__hash__()` is called for the first time.
6060
'_hash',
6161
)
62+
pathmod = os.path
6263

6364
def __new__(cls, *args, **kwargs):
6465
"""Construct a PurePath from one or several strings and or existing

Lib/pathlib/_abc.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import functools
22
import io
33
import ntpath
4-
import os
54
import posixpath
65
import sys
76
import warnings
@@ -204,7 +203,7 @@ class PurePathBase:
204203
# work from occurring when `resolve()` calls `stat()` or `readlink()`.
205204
'_resolving',
206205
)
207-
pathmod = os.path
206+
pathmod = posixpath
208207

209208
def __init__(self, *paths):
210209
self._raw_paths = paths

Lib/test/test_pathlib/test_pathlib.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
import os
33
import sys
44
import errno
5+
import ntpath
56
import pathlib
67
import pickle
8+
import posixpath
79
import socket
810
import stat
911
import tempfile
@@ -39,6 +41,50 @@
3941
class PurePathTest(test_pathlib_abc.DummyPurePathTest):
4042
cls = pathlib.PurePath
4143

44+
# Make sure any symbolic links in the base test path are resolved.
45+
base = os.path.realpath(TESTFN)
46+
47+
def test_concrete_class(self):
48+
if self.cls is pathlib.PurePath:
49+
expected = pathlib.PureWindowsPath if os.name == 'nt' else pathlib.PurePosixPath
50+
else:
51+
expected = self.cls
52+
p = self.cls('a')
53+
self.assertIs(type(p), expected)
54+
55+
def test_concrete_pathmod(self):
56+
if self.cls is pathlib.PurePosixPath:
57+
expected = posixpath
58+
elif self.cls is pathlib.PureWindowsPath:
59+
expected = ntpath
60+
else:
61+
expected = os.path
62+
p = self.cls('a')
63+
self.assertIs(p.pathmod, expected)
64+
65+
def test_different_pathmods_unequal(self):
66+
p = self.cls('a')
67+
if p.pathmod is posixpath:
68+
q = pathlib.PureWindowsPath('a')
69+
else:
70+
q = pathlib.PurePosixPath('a')
71+
self.assertNotEqual(p, q)
72+
73+
def test_different_pathmods_unordered(self):
74+
p = self.cls('a')
75+
if p.pathmod is posixpath:
76+
q = pathlib.PureWindowsPath('a')
77+
else:
78+
q = pathlib.PurePosixPath('a')
79+
with self.assertRaises(TypeError):
80+
p < q
81+
with self.assertRaises(TypeError):
82+
p <= q
83+
with self.assertRaises(TypeError):
84+
p > q
85+
with self.assertRaises(TypeError):
86+
p >= q
87+
4288
def test_constructor_nested(self):
4389
P = self.cls
4490
P(FakePath("a/b/c"))
@@ -958,6 +1004,19 @@ def tempdir(self):
9581004
self.addCleanup(os_helper.rmtree, d)
9591005
return d
9601006

1007+
def test_matches_pathbase_api(self):
1008+
our_names = {name for name in dir(self.cls) if name[0] != '_'}
1009+
path_names = {name for name in dir(pathlib._abc.PathBase) if name[0] != '_'}
1010+
self.assertEqual(our_names, path_names)
1011+
for attr_name in our_names:
1012+
if attr_name == 'pathmod':
1013+
# On Windows, Path.pathmod is ntpath, but PathBase.pathmod is
1014+
# posixpath, and so their docstrings differ.
1015+
continue
1016+
our_attr = getattr(self.cls, attr_name)
1017+
path_attr = getattr(pathlib._abc.PathBase, attr_name)
1018+
self.assertEqual(our_attr.__doc__, path_attr.__doc__)
1019+
9611020
def test_concrete_class(self):
9621021
if self.cls is pathlib.Path:
9631022
expected = pathlib.WindowsPath if os.name == 'nt' else pathlib.PosixPath

Lib/test/test_pathlib/test_pathlib_abc.py

Lines changed: 26 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ def test_magic_methods(self):
3838
self.assertIs(P.__gt__, object.__gt__)
3939
self.assertIs(P.__ge__, object.__ge__)
4040

41+
def test_pathmod(self):
42+
self.assertIs(self.cls.pathmod, posixpath)
43+
4144

4245
class DummyPurePath(pathlib._abc.PurePathBase):
4346
def __eq__(self, other):
@@ -52,8 +55,8 @@ def __hash__(self):
5255
class DummyPurePathTest(unittest.TestCase):
5356
cls = DummyPurePath
5457

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

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

89-
def test_concrete_class(self):
90-
if self.cls is pathlib.PurePath:
91-
expected = pathlib.PureWindowsPath if os.name == 'nt' else pathlib.PurePosixPath
92-
else:
93-
expected = self.cls
94-
p = self.cls('a')
95-
self.assertIs(type(p), expected)
96-
97-
def test_different_pathmods_unequal(self):
98-
p = self.cls('a')
99-
if p.pathmod is posixpath:
100-
q = pathlib.PureWindowsPath('a')
101-
else:
102-
q = pathlib.PurePosixPath('a')
103-
self.assertNotEqual(p, q)
104-
105-
def test_different_pathmods_unordered(self):
106-
p = self.cls('a')
107-
if p.pathmod is posixpath:
108-
q = pathlib.PureWindowsPath('a')
109-
else:
110-
q = pathlib.PurePosixPath('a')
111-
with self.assertRaises(TypeError):
112-
p < q
113-
with self.assertRaises(TypeError):
114-
p <= q
115-
with self.assertRaises(TypeError):
116-
p > q
117-
with self.assertRaises(TypeError):
118-
p >= q
119-
12092
def _check_str_subclass(self, *args):
12193
# Issue #21127: it should be possible to construct a PurePath object
12294
# from a str subclass instance, and it then gets converted to
@@ -721,15 +693,6 @@ def test_fspath_common(self):
721693
def test_as_bytes_common(self):
722694
self.assertRaises(TypeError, bytes, self.cls())
723695

724-
def test_matches_path_api(self):
725-
our_names = {name for name in dir(self.cls) if name[0] != '_'}
726-
path_names = {name for name in dir(pathlib.Path) if name[0] != '_'}
727-
self.assertEqual(our_names, path_names)
728-
for attr_name in our_names:
729-
our_attr = getattr(self.cls, attr_name)
730-
path_attr = getattr(pathlib.Path, attr_name)
731-
self.assertEqual(our_attr.__doc__, path_attr.__doc__)
732-
733696

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

907870
def assertEqualNormCase(self, path_a, path_b):
908-
self.assertEqual(os.path.normcase(path_a), os.path.normcase(path_b))
871+
normcase = self.pathmod.normcase
872+
self.assertEqual(normcase(path_a), normcase(path_b))
909873

910874
def test_samefile(self):
911-
fileA_path = os.path.join(self.base, 'fileA')
912-
fileB_path = os.path.join(self.base, 'dirB', 'fileB')
875+
pathmod = self.pathmod
876+
fileA_path = pathmod.join(self.base, 'fileA')
877+
fileB_path = pathmod.join(self.base, 'dirB', 'fileB')
913878
p = self.cls(fileA_path)
914879
pp = self.cls(fileA_path)
915880
q = self.cls(fileB_path)
@@ -918,7 +883,7 @@ def test_samefile(self):
918883
self.assertFalse(p.samefile(fileB_path))
919884
self.assertFalse(p.samefile(q))
920885
# Test the non-existent file case
921-
non_existent = os.path.join(self.base, 'foo')
886+
non_existent = pathmod.join(self.base, 'foo')
922887
r = self.cls(non_existent)
923888
self.assertRaises(FileNotFoundError, p.samefile, r)
924889
self.assertRaises(FileNotFoundError, p.samefile, non_existent)
@@ -1379,14 +1344,15 @@ def test_resolve_common(self):
13791344
p.resolve(strict=True)
13801345
self.assertEqual(cm.exception.errno, errno.ENOENT)
13811346
# Non-strict
1347+
pathmod = self.pathmod
13821348
self.assertEqualNormCase(str(p.resolve(strict=False)),
1383-
os.path.join(self.base, 'foo'))
1349+
pathmod.join(self.base, 'foo'))
13841350
p = P(self.base, 'foo', 'in', 'spam')
13851351
self.assertEqualNormCase(str(p.resolve(strict=False)),
1386-
os.path.join(self.base, 'foo', 'in', 'spam'))
1352+
pathmod.join(self.base, 'foo', 'in', 'spam'))
13871353
p = P(self.base, '..', 'foo', 'in', 'spam')
13881354
self.assertEqualNormCase(str(p.resolve(strict=False)),
1389-
os.path.abspath(os.path.join('foo', 'in', 'spam')))
1355+
pathmod.join(pathmod.dirname(self.base), 'foo', 'in', 'spam'))
13901356
# These are all relative symlinks.
13911357
p = P(self.base, 'dirB', 'fileB')
13921358
self._check_resolve_relative(p, p)
@@ -1401,7 +1367,7 @@ def test_resolve_common(self):
14011367
self._check_resolve_relative(p, P(self.base, 'dirB', 'fileB', 'foo', 'in',
14021368
'spam'), False)
14031369
p = P(self.base, 'dirA', 'linkC', '..', 'foo', 'in', 'spam')
1404-
if os.name == 'nt' and isinstance(p, pathlib.Path):
1370+
if self.cls.pathmod is not posixpath:
14051371
# In Windows, if linkY points to dirB, 'dirA\linkY\..'
14061372
# resolves to 'dirA' without resolving linkY first.
14071373
self._check_resolve_relative(p, P(self.base, 'dirA', 'foo', 'in',
@@ -1421,7 +1387,7 @@ def test_resolve_common(self):
14211387
self._check_resolve_relative(p, P(self.base, 'dirB', 'foo', 'in', 'spam'),
14221388
False)
14231389
p = P(self.base, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam')
1424-
if os.name == 'nt' and isinstance(p, pathlib.Path):
1390+
if self.cls.pathmod is not posixpath:
14251391
# In Windows, if linkY points to dirB, 'dirA\linkY\..'
14261392
# resolves to 'dirA' without resolving linkY first.
14271393
self._check_resolve_relative(p, P(d, 'foo', 'in', 'spam'), False)
@@ -1434,10 +1400,11 @@ def test_resolve_dot(self):
14341400
# See http://web.archive.org/web/20200623062557/https://bitbucket.org/pitrou/pathlib/issues/9/
14351401
if not self.can_symlink:
14361402
self.skipTest("symlinks required")
1403+
pathmod = self.pathmod
14371404
p = self.cls(self.base)
14381405
p.joinpath('0').symlink_to('.', target_is_directory=True)
1439-
p.joinpath('1').symlink_to(os.path.join('0', '0'), target_is_directory=True)
1440-
p.joinpath('2').symlink_to(os.path.join('1', '1'), target_is_directory=True)
1406+
p.joinpath('1').symlink_to(pathmod.join('0', '0'), target_is_directory=True)
1407+
p.joinpath('2').symlink_to(pathmod.join('1', '1'), target_is_directory=True)
14411408
q = p / '2'
14421409
self.assertEqual(q.resolve(strict=True), p)
14431410
r = q / '3' / '4'
@@ -1454,7 +1421,7 @@ def _check_symlink_loop(self, *args):
14541421
def test_resolve_loop(self):
14551422
if not self.can_symlink:
14561423
self.skipTest("symlinks required")
1457-
if os.name == 'nt' and issubclass(self.cls, pathlib.Path):
1424+
if self.cls.pathmod is not posixpath:
14581425
self.skipTest("symlink loops work differently with concrete Windows paths")
14591426
# Loops with relative symlinks.
14601427
self.cls(self.base, 'linkX').symlink_to('linkX/inside')
@@ -1657,10 +1624,11 @@ def _check_complex_symlinks(self, link0_target):
16571624
self.skipTest("symlinks required")
16581625

16591626
# Test solving a non-looping chain of symlinks (issue #19887).
1627+
pathmod = self.pathmod
16601628
P = self.cls(self.base)
1661-
P.joinpath('link1').symlink_to(os.path.join('link0', 'link0'), target_is_directory=True)
1662-
P.joinpath('link2').symlink_to(os.path.join('link1', 'link1'), target_is_directory=True)
1663-
P.joinpath('link3').symlink_to(os.path.join('link2', 'link2'), target_is_directory=True)
1629+
P.joinpath('link1').symlink_to(pathmod.join('link0', 'link0'), target_is_directory=True)
1630+
P.joinpath('link2').symlink_to(pathmod.join('link1', 'link1'), target_is_directory=True)
1631+
P.joinpath('link3').symlink_to(pathmod.join('link2', 'link2'), target_is_directory=True)
16641632
P.joinpath('link0').symlink_to(link0_target, target_is_directory=True)
16651633

16661634
# Resolve absolute paths.
@@ -1707,7 +1675,7 @@ def test_complex_symlinks_relative(self):
17071675
self._check_complex_symlinks('.')
17081676

17091677
def test_complex_symlinks_relative_dot_dot(self):
1710-
self._check_complex_symlinks(os.path.join('dirA', '..'))
1678+
self._check_complex_symlinks(self.pathmod.join('dirA', '..'))
17111679

17121680
def setUpWalk(self):
17131681
# Build:

0 commit comments

Comments
 (0)