Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
GH-65238: Fix stripping of trailing slash in pathlib
This brings pathlib in line with *IEEE Std 1003.1-2017*, where trailing
slashes are meaningful to path resolution and should not be discarded.

See https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_13
  • Loading branch information
barneygale committed Apr 17, 2023
commit e2e0a4940ce4c56a1ef8818f2f06433a6b7ce237
5 changes: 4 additions & 1 deletion Lib/importlib/metadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -748,7 +748,10 @@ def read_text(self, filename):
NotADirectoryError,
PermissionError,
):
return self._path.joinpath(filename).read_text(encoding='utf-8')
path = self._path
if filename:
path /= filename
return path.read_text(encoding='utf-8')

read_text.__doc__ = Distribution.read_text.__doc__

Expand Down
13 changes: 9 additions & 4 deletions Lib/pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,9 @@ def _parse_path(cls, path):
# pathlib assumes that UNC paths always have a root.
root = sep
parsed = [sys.intern(str(x)) for x in rel.split(sep) if x and x != '.']
if parsed and not rel.endswith(parsed[-1]):
# Preserve trailing slash
parsed.append('')
return drv, root, parsed

def _load_parts(self):
Expand Down Expand Up @@ -578,6 +581,9 @@ def relative_to(self, other, /, *_deprecated, walk_up=False):
remove=(3, 14))
path_cls = type(self)
other = path_cls(other, *_deprecated)
if not other.name:
# Ignore trailing slash.
other = other.parent
for step, path in enumerate([other] + list(other.parents)):
if self.is_relative_to(path):
break
Expand All @@ -598,6 +604,9 @@ def is_relative_to(self, other, /, *_deprecated):
warnings._deprecated("pathlib.PurePath.is_relative_to(*args)",
msg, remove=(3, 14))
other = type(self)(other, *_deprecated)
if not other.name:
# Ignore trailing slash.
other = other.parent
return other == self or other in self.parents

@property
Expand Down Expand Up @@ -825,8 +834,6 @@ def glob(self, pattern):
drv, root, pattern_parts = self._parse_path(pattern)
if drv or root:
raise NotImplementedError("Non-relative patterns are unsupported")
if pattern[-1] in (self._flavour.sep, self._flavour.altsep):
pattern_parts.append('')
selector = _make_selector(tuple(pattern_parts), self._flavour)
for p in selector.select_from(self):
yield p
Expand All @@ -840,8 +847,6 @@ def rglob(self, pattern):
drv, root, pattern_parts = self._parse_path(pattern)
if drv or root:
raise NotImplementedError("Non-relative patterns are unsupported")
if pattern and pattern[-1] in (self._flavour.sep, self._flavour.altsep):
pattern_parts.append('')
selector = _make_selector(("**",) + tuple(pattern_parts), self._flavour)
for p in selector.select_from(self):
yield p
Expand Down
66 changes: 54 additions & 12 deletions Lib/test/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,14 @@ class _BasePurePathTest(object):
# supposed to produce equal paths.
equivalences = {
'a/b': [
('a', 'b'), ('a/', 'b'), ('a', 'b/'), ('a/', 'b/'),
('a/b/',), ('a//b',), ('a//b//',),
('a', 'b'), ('a/', 'b'), ('a//b',),
# Empty components get removed.
('', 'a', 'b'), ('a', '', 'b'), ('a', 'b', ''),
('', 'a', 'b'), ('a', '', 'b'),
],
'a/b/': [
('a', 'b/'), ('a/', 'b/'), ('a/b/',),
('a//b//',), ('a', 'b', ''),
],
'/b/c/d': [
('a', '/b/c', 'd'), ('/a', '/b/c', 'd'),
# Empty components get removed.
Expand Down Expand Up @@ -154,11 +157,11 @@ def test_drive_root_parts_common(self):
# Unanchored parts.
check((), '', '', ())
check(('a',), '', '', ('a',))
check(('a/',), '', '', ('a',))
check(('a/',), '', '', ('a', ''))
check(('a', 'b'), '', '', ('a', 'b'))
# Expansion.
check(('a/b',), '', '', ('a', 'b'))
check(('a/b/',), '', '', ('a', 'b'))
check(('a/b/',), '', '', ('a', 'b', ''))
check(('a', 'b/c', 'd'), '', '', ('a', 'b', 'c', 'd'))
# Collapsing and stripping excess slashes.
check(('a', 'b//c', 'd'), '', '', ('a', 'b', 'c', 'd'))
Expand All @@ -167,7 +170,7 @@ def test_drive_root_parts_common(self):
check(('.',), '', '', ())
check(('.', '.', 'b'), '', '', ('b',))
check(('a', '.', 'b'), '', '', ('a', 'b'))
check(('a', '.', '.'), '', '', ('a',))
check(('a', '.', '.'), '', '', ('a', ''))
# The first part is anchored.
check(('/a/b',), '', sep, (sep, 'a', 'b'))
check(('/a', 'b'), '', sep, (sep, 'a', 'b'))
Expand All @@ -188,6 +191,24 @@ def test_join_common(self):
self.assertEqual(pp, P('a/b/c'))
pp = p.joinpath('/c')
self.assertEqual(pp, P('/c'))
pp = p.joinpath('.')
self.assertEqual(pp, P('a/b/'))
pp = p.joinpath('')
self.assertEqual(pp, P('a/b/'))
p = P('a/b/')
pp = p.joinpath('c')
self.assertEqual(pp, P('a/b/c'))
self.assertIs(type(pp), type(p))
pp = p.joinpath('c', 'd')
self.assertEqual(pp, P('a/b/c/d'))
pp = p.joinpath(P('c'))
self.assertEqual(pp, P('a/b/c'))
pp = p.joinpath('/c')
self.assertEqual(pp, P('/c'))
pp = p.joinpath('.')
self.assertEqual(pp, P('a/b/'))
pp = p.joinpath('')
self.assertEqual(pp, P('a/b/'))

def test_div_common(self):
# Basically the same as joinpath().
Expand Down Expand Up @@ -389,6 +410,12 @@ def test_parent_common(self):
self.assertEqual(p.parent.parent, P('/a'))
self.assertEqual(p.parent.parent.parent, P('/'))
self.assertEqual(p.parent.parent.parent.parent, P('/'))
# Trailing slash
p = P('/a/b/')
self.assertEqual(p.parent, P('/a/b'))
self.assertEqual(p.parent.parent, P('/a'))
self.assertEqual(p.parent.parent.parent, P('/'))
self.assertEqual(p.parent.parent.parent.parent, P('/'))

def test_parents_common(self):
# Relative
Expand Down Expand Up @@ -436,6 +463,9 @@ def test_parents_common(self):
par[-4]
with self.assertRaises(IndexError):
par[3]
# Trailing slash
self.assertEqual(P('a/b/').parents[:], (P('a/b'), P('a'), P()))
self.assertEqual(P('/a/b/').parents[:], (P('/a/b'), P('/a'), P('/')))

def test_drive_common(self):
P = self.cls
Expand Down Expand Up @@ -466,7 +496,7 @@ def test_name_common(self):
self.assertEqual(P('/').name, '')
self.assertEqual(P('a/b').name, 'b')
self.assertEqual(P('/a/b').name, 'b')
self.assertEqual(P('/a/b/.').name, 'b')
self.assertEqual(P('/a/b/.').name, '')
self.assertEqual(P('a/b.py').name, 'b.py')
self.assertEqual(P('/a/b.py').name, 'b.py')

Expand Down Expand Up @@ -534,6 +564,7 @@ def test_with_name_common(self):
self.assertRaises(ValueError, P('').with_name, 'd.xml')
self.assertRaises(ValueError, P('.').with_name, 'd.xml')
self.assertRaises(ValueError, P('/').with_name, 'd.xml')
self.assertRaises(ValueError, P('a/').with_name, 'd.xml')
self.assertRaises(ValueError, P('a/b').with_name, '')
self.assertRaises(ValueError, P('a/b').with_name, '/c')
self.assertRaises(ValueError, P('a/b').with_name, 'c/')
Expand All @@ -551,6 +582,7 @@ def test_with_stem_common(self):
self.assertRaises(ValueError, P('').with_stem, 'd')
self.assertRaises(ValueError, P('.').with_stem, 'd')
self.assertRaises(ValueError, P('/').with_stem, 'd')
self.assertRaises(ValueError, P('a/').with_stem, 'd')
self.assertRaises(ValueError, P('a/b').with_stem, '')
self.assertRaises(ValueError, P('a/b').with_stem, '/c')
self.assertRaises(ValueError, P('a/b').with_stem, 'c/')
Expand All @@ -569,6 +601,7 @@ def test_with_suffix_common(self):
self.assertRaises(ValueError, P('').with_suffix, '.gz')
self.assertRaises(ValueError, P('.').with_suffix, '.gz')
self.assertRaises(ValueError, P('/').with_suffix, '.gz')
self.assertRaises(ValueError, P('a/').with_suffix, '.gz')
# Invalid suffix.
self.assertRaises(ValueError, P('a/b').with_suffix, 'gz')
self.assertRaises(ValueError, P('a/b').with_suffix, '/')
Expand Down Expand Up @@ -789,7 +822,8 @@ class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase):
equivalences = _BasePurePathTest.equivalences.copy()
equivalences.update({
'./a:b': [ ('./a:b',) ],
'c:a': [ ('c:', 'a'), ('c:', 'a/'), ('.', 'c:', 'a') ],
'c:a': [ ('c:', 'a'), ('.', 'c:', 'a') ],
'c:a/': [ ('c:', 'a/') ],
'c:/a': [
('c:/', 'a'), ('c:', '/', 'a'), ('c:', '/a'),
('/z', 'c:/', 'a'), ('//x/y', 'c:/', 'a'),
Expand Down Expand Up @@ -819,7 +853,7 @@ def test_drive_root_parts(self):
# UNC paths.
check(('a', '//b/c', 'd'), '\\\\b\\c', '\\', ('\\\\b\\c\\', 'd'))
# Collapsing and stripping excess slashes.
check(('a', 'Z://b//c/', 'd/'), 'Z:', '\\', ('Z:\\', 'b', 'c', 'd'))
check(('a', 'Z://b//c/', 'd/'), 'Z:', '\\', ('Z:\\', 'b', 'c', 'd', ''))
# UNC paths.
check(('a', '//b/c//', 'd'), '\\\\b\\c', '\\', ('\\\\b\\c\\', 'd'))
# Extended paths.
Expand Down Expand Up @@ -970,11 +1004,15 @@ def test_parent(self):
self.assertEqual(p.parent, P('//a/b/c'))
self.assertEqual(p.parent.parent, P('//a/b'))
self.assertEqual(p.parent.parent.parent, P('//a/b'))
# Trailing slash
self.assertEqual(P('z:a/b/').parent, P('z:a/b'))
self.assertEqual(P('z:/a/b/').parent, P('z:/a/b'))
self.assertEqual(P('//a/b/c/d/').parent, P('//a/b/c/d'))

def test_parents(self):
# Anchored
P = self.cls
p = P('z:a/b/')
p = P('z:a/b')
par = p.parents
self.assertEqual(len(par), 2)
self.assertEqual(par[0], P('z:a'))
Expand All @@ -988,7 +1026,7 @@ def test_parents(self):
self.assertEqual(list(par), [P('z:a'), P('z:')])
with self.assertRaises(IndexError):
par[2]
p = P('z:/a/b/')
p = P('z:/a/b')
par = p.parents
self.assertEqual(len(par), 2)
self.assertEqual(par[0], P('z:/a'))
Expand Down Expand Up @@ -1016,6 +1054,10 @@ def test_parents(self):
self.assertEqual(list(par), [P('//a/b/c'), P('//a/b')])
with self.assertRaises(IndexError):
par[2]
# Trailing slash
self.assertEqual(P('z:a/b/').parents[:], (P('z:a/b'), P('z:a'), P('z:')))
self.assertEqual(P('z:/a/b/').parents[:], (P('z:/a/b'), P('z:/a'), P('z:/')))
self.assertEqual(P('//a/b/c/d/').parents[:], (P('//a/b/c/d'), P('//a/b/c'), P('//a/b/')))

def test_drive(self):
P = self.cls
Expand Down Expand Up @@ -1790,7 +1832,7 @@ def _check(glob, expected):

def test_rglob_common(self):
def _check(glob, expected):
self.assertEqual(set(glob), { P(BASE, q) for q in expected })
self.assertEqual(set(glob), { P(BASE, q) if q else P(BASE) for q in expected })
P = self.cls
p = P(BASE)
it = p.rglob("fileA")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix issue where :mod:`pathlib` did not preserve trailing slashes.