Issue #19717: Makes Path.resolve() succeed on paths that do not exist… · python/cpython@98eb360 · GitHub
Skip to content

Commit 98eb360

Browse files
committed
Issue #19717: Makes Path.resolve() succeed on paths that do not exist (patch by Vajrasky Kok)
1 parent 954c7dd commit 98eb360

4 files changed

Lines changed: 81 additions & 17 deletions

File tree

Doc/library/pathlib.rst

Lines changed: 8 additions & 4 deletions

Lib/pathlib.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -178,12 +178,26 @@ def casefold(self, s):
178178
def casefold_parts(self, parts):
179179
return [p.lower() for p in parts]
180180

181-
def resolve(self, path):
181+
def resolve(self, path, strict=False):
182182
s = str(path)
183183
if not s:
184184
return os.getcwd()
185+
previous_s = None
185186
if _getfinalpathname is not None:
186-
return self._ext_to_normal(_getfinalpathname(s))
187+
if strict:
188+
return self._ext_to_normal(_getfinalpathname(s))
189+
else:
190+
while True:
191+
try:
192+
s = self._ext_to_normal(_getfinalpathname(s))
193+
except FileNotFoundError:
194+
previous_s = s
195+
s = os.path.abspath(os.path.join(s, os.pardir))
196+
else:
197+
if previous_s is None:
198+
return s
199+
else:
200+
return s + os.path.sep + os.path.basename(previous_s)
187201
# Means fallback on absolute
188202
return None
189203

@@ -285,7 +299,7 @@ def casefold(self, s):
285299
def casefold_parts(self, parts):
286300
return parts
287301

288-
def resolve(self, path):
302+
def resolve(self, path, strict=False):
289303
sep = self.sep
290304
accessor = path._accessor
291305
seen = {}
@@ -315,7 +329,10 @@ def _resolve(path, rest):
315329
target = accessor.readlink(newpath)
316330
except OSError as e:
317331
if e.errno != EINVAL:
318-
raise
332+
if strict:
333+
raise
334+
else:
335+
return newpath
319336
# Not a symlink
320337
path = newpath
321338
else:
@@ -1092,15 +1109,15 @@ def absolute(self):
10921109
obj._init(template=self)
10931110
return obj
10941111

1095-
def resolve(self):
1112+
def resolve(self, strict=False):
10961113
"""
10971114
Make the path absolute, resolving all symlinks on the way and also
10981115
normalizing it (for example turning slashes into backslashes under
10991116
Windows).
11001117
"""
11011118
if self._closed:
11021119
self._raise_closed()
1103-
s = self._flavour.resolve(self)
1120+
s = self._flavour.resolve(self, strict=strict)
11041121
if s is None:
11051122
# No symlink resolution => for consistency, raise an error if
11061123
# the path doesn't exist or is forbidden

Lib/test/test_pathlib.py

Lines changed: 47 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1486,8 +1486,8 @@ def test_glob_dotdot(self):
14861486
self.assertEqual(set(p.glob("../xyzzy")), set())
14871487

14881488

1489-
def _check_resolve(self, p, expected):
1490-
q = p.resolve()
1489+
def _check_resolve(self, p, expected, strict=True):
1490+
q = p.resolve(strict)
14911491
self.assertEqual(q, expected)
14921492

14931493
# this can be used to check both relative and absolute resolutions
@@ -1498,8 +1498,17 @@ def test_resolve_common(self):
14981498
P = self.cls
14991499
p = P(BASE, 'foo')
15001500
with self.assertRaises(OSError) as cm:
1501-
p.resolve()
1501+
p.resolve(strict=True)
15021502
self.assertEqual(cm.exception.errno, errno.ENOENT)
1503+
# Non-strict
1504+
self.assertEqual(str(p.resolve(strict=False)),
1505+
os.path.join(BASE, 'foo'))
1506+
p = P(BASE, 'foo', 'in', 'spam')
1507+
self.assertEqual(str(p.resolve(strict=False)),
1508+
os.path.join(BASE, 'foo'))
1509+
p = P(BASE, '..', 'foo', 'in', 'spam')
1510+
self.assertEqual(str(p.resolve(strict=False)),
1511+
os.path.abspath(os.path.join('foo')))
15031512
# These are all relative symlinks
15041513
p = P(BASE, 'dirB', 'fileB')
15051514
self._check_resolve_relative(p, p)
@@ -1509,13 +1518,37 @@ def test_resolve_common(self):
15091518
self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB'))
15101519
p = P(BASE, 'dirB', 'linkD', 'fileB')
15111520
self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB'))
1521+
# Non-strict
1522+
p = P(BASE, 'dirA', 'linkC', 'fileB', 'foo', 'in', 'spam')
1523+
self._check_resolve_relative(p, P(BASE, 'dirB', 'fileB', 'foo'), False)
1524+
p = P(BASE, 'dirA', 'linkC', '..', 'foo', 'in', 'spam')
1525+
if os.name == 'nt':
1526+
# In Windows, if linkY points to dirB, 'dirA\linkY\..'
1527+
# resolves to 'dirA' without resolving linkY first.
1528+
self._check_resolve_relative(p, P(BASE, 'dirA', 'foo'), False)
1529+
else:
1530+
# In Posix, if linkY points to dirB, 'dirA/linkY/..'
1531+
# resolves to 'dirB/..' first before resolving to parent of dirB.
1532+
self._check_resolve_relative(p, P(BASE, 'foo'), False)
15121533
# Now create absolute symlinks
15131534
d = tempfile.mkdtemp(suffix='-dirD')
15141535
self.addCleanup(support.rmtree, d)
15151536
os.symlink(os.path.join(d), join('dirA', 'linkX'))
15161537
os.symlink(join('dirB'), os.path.join(d, 'linkY'))
15171538
p = P(BASE, 'dirA', 'linkX', 'linkY', 'fileB')
15181539
self._check_resolve_absolute(p, P(BASE, 'dirB', 'fileB'))
1540+
# Non-strict
1541+
p = P(BASE, 'dirA', 'linkX', 'linkY', 'foo', 'in', 'spam')
1542+
self._check_resolve_relative(p, P(BASE, 'dirB', 'foo'), False)
1543+
p = P(BASE, 'dirA', 'linkX', 'linkY', '..', 'foo', 'in', 'spam')
1544+
if os.name == 'nt':
1545+
# In Windows, if linkY points to dirB, 'dirA\linkY\..'
1546+
# resolves to 'dirA' without resolving linkY first.
1547+
self._check_resolve_relative(p, P(d, 'foo'), False)
1548+
else:
1549+
# In Posix, if linkY points to dirB, 'dirA/linkY/..'
1550+
# resolves to 'dirB/..' first before resolving to parent of dirB.
1551+
self._check_resolve_relative(p, P(BASE, 'foo'), False)
15191552

15201553
@with_symlinks
15211554
def test_resolve_dot(self):
@@ -1525,7 +1558,11 @@ def test_resolve_dot(self):
15251558
self.dirlink(os.path.join('0', '0'), join('1'))
15261559
self.dirlink(os.path.join('1', '1'), join('2'))
15271560
q = p / '2'
1528-
self.assertEqual(q.resolve(), p)
1561+
self.assertEqual(q.resolve(strict=True), p)
1562+
r = q / '3' / '4'
1563+
self.assertRaises(FileNotFoundError, r.resolve, strict=True)
1564+
# Non-strict
1565+
self.assertEqual(r.resolve(strict=False), p / '3')
15291566

15301567
def test_with(self):
15311568
p = self.cls(BASE)
@@ -1972,10 +2009,10 @@ def test_glob_empty_pattern(self):
19722009
class PosixPathTest(_BasePathTest, unittest.TestCase):
19732010
cls = pathlib.PosixPath
19742011

1975-
def _check_symlink_loop(self, *args):
2012+
def _check_symlink_loop(self, *args, strict=True):
19762013
path = self.cls(*args)
19772014
with self.assertRaises(RuntimeError):
1978-
print(path.resolve())
2015+
print(path.resolve(strict))
19792016

19802017
def test_open_mode(self):
19812018
old_mask = os.umask(0)
@@ -2008,21 +2045,24 @@ def test_touch_mode(self):
20082045

20092046
@with_symlinks
20102047
def test_resolve_loop(self):
2011-
# Loop detection for broken symlinks under POSIX
20122048
# Loops with relative symlinks
20132049
os.symlink('linkX/inside', join('linkX'))
20142050
self._check_symlink_loop(BASE, 'linkX')
20152051
os.symlink('linkY', join('linkY'))
20162052
self._check_symlink_loop(BASE, 'linkY')
20172053
os.symlink('linkZ/../linkZ', join('linkZ'))
20182054
self._check_symlink_loop(BASE, 'linkZ')
2055+
# Non-strict
2056+
self._check_symlink_loop(BASE, 'linkZ', 'foo', strict=False)
20192057
# Loops with absolute symlinks
20202058
os.symlink(join('linkU/inside'), join('linkU'))
20212059
self._check_symlink_loop(BASE, 'linkU')
20222060
os.symlink(join('linkV'), join('linkV'))
20232061
self._check_symlink_loop(BASE, 'linkV')
20242062
os.symlink(join('linkW/../linkW'), join('linkW'))
20252063
self._check_symlink_loop(BASE, 'linkW')
2064+
# Non-strict
2065+
self._check_symlink_loop(BASE, 'linkW', 'foo', strict=False)
20262066

20272067
def test_glob(self):
20282068
P = self.cls

Misc/NEWS

Lines changed: 3 additions & 0 deletions

0 commit comments

Comments
 (0)