Skip to content

Commit 9fb0f2d

Browse files
authored
GH-110109: Speed up pathlib._PathBase.resolve() (#110412)
- Add fast path to `_split_stack()` - Skip unnecessarily resolution of the current directory when a relative path is given to `resolve()` - Remove stat and target caches, which slow down most `resolve()` calls in practice. - Slightly refactor code for clarity.
1 parent 25538c7 commit 9fb0f2d

File tree

1 file changed

+17
-22
lines changed

1 file changed

+17
-22
lines changed

Lib/pathlib.py

+17-22
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,8 @@ def _split_stack(self):
11821182
uppermost parent of the path (equivalent to path.parents[-1]), and
11831183
*parts* is a reversed list of parts following the anchor.
11841184
"""
1185+
if not self._tail:
1186+
return self, []
11851187
return self._from_parsed_parts(self.drive, self.root, []), self._tail[::-1]
11861188

11871189
def resolve(self, strict=False):
@@ -1191,18 +1193,16 @@ def resolve(self, strict=False):
11911193
"""
11921194
if self._resolving:
11931195
return self
1196+
path, parts = self._split_stack()
11941197
try:
1195-
path = self.absolute()
1198+
path = path.absolute()
11961199
except UnsupportedOperation:
1197-
path = self
1200+
pass
11981201

11991202
# If the user has *not* overridden the `readlink()` method, then symlinks are unsupported
12001203
# and (in non-strict mode) we can improve performance by not calling `stat()`.
12011204
querying = strict or getattr(self.readlink, '_supported', True)
12021205
link_count = 0
1203-
stat_cache = {}
1204-
target_cache = {}
1205-
path, parts = path._split_stack()
12061206
while parts:
12071207
part = parts.pop()
12081208
if part == '..':
@@ -1214,40 +1214,35 @@ def resolve(self, strict=False):
12141214
# Delete '..' segment and its predecessor
12151215
path = path.parent
12161216
continue
1217-
# Join the current part onto the path.
1218-
path_parent = path
1219-
path = path._make_child_relpath(part)
1217+
next_path = path._make_child_relpath(part)
12201218
if querying and part != '..':
1221-
path._resolving = True
1219+
next_path._resolving = True
12221220
try:
1223-
st = stat_cache.get(path)
1224-
if st is None:
1225-
st = stat_cache[path] = path.stat(follow_symlinks=False)
1221+
st = next_path.stat(follow_symlinks=False)
12261222
if S_ISLNK(st.st_mode):
12271223
# Like Linux and macOS, raise OSError(errno.ELOOP) if too many symlinks are
12281224
# encountered during resolution.
12291225
link_count += 1
12301226
if link_count >= _MAX_SYMLINKS:
1231-
raise OSError(ELOOP, "Too many symbolic links in path", str(path))
1232-
target = target_cache.get(path)
1233-
if target is None:
1234-
target = target_cache[path] = path.readlink()
1235-
target, target_parts = target._split_stack()
1227+
raise OSError(ELOOP, "Too many symbolic links in path", str(self))
1228+
target, target_parts = next_path.readlink()._split_stack()
12361229
# If the symlink target is absolute (like '/etc/hosts'), set the current
1237-
# path to its uppermost parent (like '/'). If not, the symlink target is
1238-
# relative to the symlink parent, which we recorded earlier.
1239-
path = target if target.root else path_parent
1230+
# path to its uppermost parent (like '/').
1231+
if target.root:
1232+
path = target
12401233
# Add the symlink target's reversed tail parts (like ['hosts', 'etc']) to
12411234
# the stack of unresolved path parts.
12421235
parts.extend(target_parts)
1236+
continue
12431237
elif parts and not S_ISDIR(st.st_mode):
1244-
raise NotADirectoryError(ENOTDIR, "Not a directory", str(path))
1238+
raise NotADirectoryError(ENOTDIR, "Not a directory", str(self))
12451239
except OSError:
12461240
if strict:
12471241
raise
12481242
else:
12491243
querying = False
1250-
path._resolving = False
1244+
next_path._resolving = False
1245+
path = next_path
12511246
return path
12521247

12531248
def symlink_to(self, target, target_is_directory=False):

0 commit comments

Comments
 (0)