Skip to content

Commit d88677a

Browse files
authored
GH-128520: More consistent type-checking behaviour in pathlib (#130199)
In the following methods, skip casting of the argument to a path object if the argument has a `with_segments` attribute. In `PurePath`: `relative_to()`, `is_relative_to()`, `match()`, and `full_match()`. In `Path`: `rename()`, `replace()`, `copy()`, `copy_into()`, `move()`, and `move_into()`. Previously the check varied a bit from method to method. The `PurePath` methods used `isinstance(arg, PurePath)`; the `rename()` and `replace()` methods always cast, and the remaining `Path` methods checked for a private `_copy_writer` attribute. We apply identical changes to relevant methods of the private ABCs. This improves performance a bit, because `isinstance()` checks on ABCs are expensive.
1 parent 286c517 commit d88677a

File tree

3 files changed

+24
-18
lines changed

3 files changed

+24
-18
lines changed

Lib/pathlib/_abc.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def full_match(self, pattern, *, case_sensitive=None):
192192
Return True if this path matches the given glob-style pattern. The
193193
pattern is matched against the entire path.
194194
"""
195-
if not isinstance(pattern, JoinablePath):
195+
if not hasattr(pattern, 'with_segments'):
196196
pattern = self.with_segments(pattern)
197197
if case_sensitive is None:
198198
case_sensitive = self.parser.normcase('Aa') == 'Aa'
@@ -286,7 +286,7 @@ def glob(self, pattern, *, case_sensitive=None, recurse_symlinks=True):
286286
"""Iterate over this subtree and yield all existing files (of any
287287
kind, including directories) matching the given relative pattern.
288288
"""
289-
if not isinstance(pattern, JoinablePath):
289+
if not hasattr(pattern, 'with_segments'):
290290
pattern = self.with_segments(pattern)
291291
anchor, parts = _explode_path(pattern)
292292
if anchor:
@@ -348,7 +348,7 @@ def copy(self, target, follow_symlinks=True, dirs_exist_ok=False,
348348
"""
349349
Recursively copy this file or directory tree to the given destination.
350350
"""
351-
if not hasattr(target, '_copy_writer'):
351+
if not hasattr(target, 'with_segments'):
352352
target = self.with_segments(target)
353353

354354
# Delegate to the target path's CopyWriter object.
@@ -366,7 +366,7 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
366366
name = self.name
367367
if not name:
368368
raise ValueError(f"{self!r} has an empty name")
369-
elif hasattr(target_dir, '_copy_writer'):
369+
elif hasattr(target_dir, 'with_segments'):
370370
target = target_dir / name
371371
else:
372372
target = self.with_segments(target_dir, name)

Lib/pathlib/_local.py

+15-14
Original file line numberDiff line numberDiff line change
@@ -475,7 +475,7 @@ def relative_to(self, other, *, walk_up=False):
475475
The *walk_up* parameter controls whether `..` may be used to resolve
476476
the path.
477477
"""
478-
if not isinstance(other, PurePath):
478+
if not hasattr(other, 'with_segments'):
479479
other = self.with_segments(other)
480480
for step, path in enumerate(chain([other], other.parents)):
481481
if path == self or path in self.parents:
@@ -492,7 +492,7 @@ def relative_to(self, other, *, walk_up=False):
492492
def is_relative_to(self, other):
493493
"""Return True if the path is relative to another path or False.
494494
"""
495-
if not isinstance(other, PurePath):
495+
if not hasattr(other, 'with_segments'):
496496
other = self.with_segments(other)
497497
return other == self or other in self.parents
498498

@@ -545,7 +545,7 @@ def full_match(self, pattern, *, case_sensitive=None):
545545
Return True if this path matches the given glob-style pattern. The
546546
pattern is matched against the entire path.
547547
"""
548-
if not isinstance(pattern, PurePath):
548+
if not hasattr(pattern, 'with_segments'):
549549
pattern = self.with_segments(pattern)
550550
if case_sensitive is None:
551551
case_sensitive = self.parser is posixpath
@@ -564,7 +564,7 @@ def match(self, path_pattern, *, case_sensitive=None):
564564
is matched. The recursive wildcard '**' is *not* supported by this
565565
method.
566566
"""
567-
if not isinstance(path_pattern, PurePath):
567+
if not hasattr(path_pattern, 'with_segments'):
568568
path_pattern = self.with_segments(path_pattern)
569569
if case_sensitive is None:
570570
case_sensitive = self.parser is posixpath
@@ -1064,7 +1064,9 @@ def rename(self, target):
10641064
Returns the new Path instance pointing to the target path.
10651065
"""
10661066
os.rename(self, target)
1067-
return self.with_segments(target)
1067+
if not hasattr(target, 'with_segments'):
1068+
target = self.with_segments(target)
1069+
return target
10681070

10691071
def replace(self, target):
10701072
"""
@@ -1077,7 +1079,9 @@ def replace(self, target):
10771079
Returns the new Path instance pointing to the target path.
10781080
"""
10791081
os.replace(self, target)
1080-
return self.with_segments(target)
1082+
if not hasattr(target, 'with_segments'):
1083+
target = self.with_segments(target)
1084+
return target
10811085

10821086
_copy_writer = property(LocalCopyWriter)
10831087

@@ -1086,7 +1090,7 @@ def copy(self, target, follow_symlinks=True, dirs_exist_ok=False,
10861090
"""
10871091
Recursively copy this file or directory tree to the given destination.
10881092
"""
1089-
if not hasattr(target, '_copy_writer'):
1093+
if not hasattr(target, 'with_segments'):
10901094
target = self.with_segments(target)
10911095

10921096
# Delegate to the target path's CopyWriter object.
@@ -1104,7 +1108,7 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
11041108
name = self.name
11051109
if not name:
11061110
raise ValueError(f"{self!r} has an empty name")
1107-
elif hasattr(target_dir, '_copy_writer'):
1111+
elif hasattr(target_dir, 'with_segments'):
11081112
target = target_dir / name
11091113
else:
11101114
target = self.with_segments(target_dir, name)
@@ -1118,16 +1122,13 @@ def move(self, target):
11181122
"""
11191123
# Use os.replace() if the target is os.PathLike and on the same FS.
11201124
try:
1121-
target_str = os.fspath(target)
1125+
target = self.with_segments(target)
11221126
except TypeError:
11231127
pass
11241128
else:
1125-
if not hasattr(target, '_copy_writer'):
1126-
target = self.with_segments(target_str)
11271129
ensure_different_files(self, target)
11281130
try:
1129-
os.replace(self, target_str)
1130-
return target
1131+
return self.replace(target)
11311132
except OSError as err:
11321133
if err.errno != EXDEV:
11331134
raise
@@ -1143,7 +1144,7 @@ def move_into(self, target_dir):
11431144
name = self.name
11441145
if not name:
11451146
raise ValueError(f"{self!r} has an empty name")
1146-
elif hasattr(target_dir, '_copy_writer'):
1147+
elif hasattr(target_dir, 'with_segments'):
11471148
target = target_dir / name
11481149
else:
11491150
target = self.with_segments(target_dir, name)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Apply type conversion consistently in :class:`pathlib.PurePath` and
2+
:class:`~pathlib.Path` methods can accept a path object as an argument, such
3+
as :meth:`~pathlib.PurePath.match` and :meth:`~pathlib.Path.rename`. The
4+
argument is now converted to path object if it lacks a
5+
:meth:`~pathlib.PurePath.with_segments` attribute, and not otherwise.

0 commit comments

Comments
 (0)