Skip to content

Commit 164dbfd

Browse files
committed
bpo-43757: make pathlib use os.path.realpath() to resolve all symlinks in a path
Removes a pathlib-specific implementation of `realpath()` that's mostly copied from `ntpath` and `posixpath`.
1 parent 1969835 commit 164dbfd

File tree

1 file changed

+10
-85
lines changed

1 file changed

+10
-85
lines changed

Lib/pathlib.py

Lines changed: 10 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -186,30 +186,6 @@ def casefold_parts(self, parts):
186186
def compile_pattern(self, pattern):
187187
return re.compile(fnmatch.translate(pattern), re.IGNORECASE).fullmatch
188188

189-
def resolve(self, path, strict=False):
190-
s = str(path)
191-
if not s:
192-
return path._accessor.getcwd()
193-
previous_s = None
194-
if _getfinalpathname is not None:
195-
if strict:
196-
return self._ext_to_normal(_getfinalpathname(s))
197-
else:
198-
tail_parts = [] # End of the path after the first one not found
199-
while True:
200-
try:
201-
s = self._ext_to_normal(_getfinalpathname(s))
202-
except FileNotFoundError:
203-
previous_s = s
204-
s, tail = os.path.split(s)
205-
tail_parts.append(tail)
206-
if previous_s == s:
207-
return path
208-
else:
209-
return os.path.join(s, *reversed(tail_parts))
210-
# Means fallback on absolute
211-
return None
212-
213189
def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix):
214190
prefix = ''
215191
if s.startswith(ext_prefix):
@@ -220,10 +196,6 @@ def _split_extended_path(self, s, ext_prefix=ext_namespace_prefix):
220196
s = '\\' + s[3:]
221197
return prefix, s
222198

223-
def _ext_to_normal(self, s):
224-
# Turn back an extended path into a normal DOS-like path
225-
return self._split_extended_path(s)[1]
226-
227199
def is_reserved(self, parts):
228200
# NOTE: the rules for reserved names seem somewhat complicated
229201
# (e.g. r"..\NUL" is reserved but not r"foo\NUL").
@@ -281,54 +253,6 @@ def casefold_parts(self, parts):
281253
def compile_pattern(self, pattern):
282254
return re.compile(fnmatch.translate(pattern)).fullmatch
283255

284-
def resolve(self, path, strict=False):
285-
sep = self.sep
286-
accessor = path._accessor
287-
seen = {}
288-
def _resolve(path, rest):
289-
if rest.startswith(sep):
290-
path = ''
291-
292-
for name in rest.split(sep):
293-
if not name or name == '.':
294-
# current dir
295-
continue
296-
if name == '..':
297-
# parent dir
298-
path, _, _ = path.rpartition(sep)
299-
continue
300-
if path.endswith(sep):
301-
newpath = path + name
302-
else:
303-
newpath = path + sep + name
304-
if newpath in seen:
305-
# Already seen this path
306-
path = seen[newpath]
307-
if path is not None:
308-
# use cached value
309-
continue
310-
# The symlink is not resolved, so we must have a symlink loop.
311-
raise RuntimeError("Symlink loop from %r" % newpath)
312-
# Resolve the symbolic link
313-
try:
314-
target = accessor.readlink(newpath)
315-
except OSError as e:
316-
if e.errno != EINVAL and strict:
317-
raise
318-
# Not a symlink, or non-strict mode. We just leave the path
319-
# untouched.
320-
path = newpath
321-
else:
322-
seen[newpath] = None # not resolved symlink
323-
path = _resolve(path, target)
324-
seen[newpath] = path # resolved symlink
325-
326-
return path
327-
# NOTE: according to POSIX, getcwd() cannot contain path components
328-
# which are symlinks.
329-
base = '' if path.is_absolute() else accessor.getcwd()
330-
return _resolve(base, str(path)) or sep
331-
332256
def is_reserved(self, parts):
333257
return False
334258

@@ -424,6 +348,8 @@ def group(self, path):
424348

425349
expanduser = staticmethod(os.path.expanduser)
426350

351+
realpath = staticmethod(os.path.realpath)
352+
427353

428354
_normal_accessor = _NormalAccessor()
429355

@@ -1132,15 +1058,14 @@ def resolve(self, strict=False):
11321058
normalizing it (for example turning slashes into backslashes under
11331059
Windows).
11341060
"""
1135-
s = self._flavour.resolve(self, strict=strict)
1136-
if s is None:
1137-
# No symlink resolution => for consistency, raise an error if
1138-
# the path doesn't exist or is forbidden
1139-
self.stat()
1140-
s = str(self.absolute())
1141-
# Now we have no symlinks in the path, it's safe to normalize it.
1142-
normed = self._flavour.pathmod.normpath(s)
1143-
return self._from_parts((normed,))
1061+
p = self._from_parts((self._accessor.realpath(self),))
1062+
try:
1063+
if S_ISLNK(self.lstat().st_mode):
1064+
raise RuntimeError("Symlink loop from %r" % str(p))
1065+
except OSError:
1066+
if strict:
1067+
raise
1068+
return p
11441069

11451070
def stat(self, *, follow_symlinks=True):
11461071
"""

0 commit comments

Comments
 (0)