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.
This commit is contained in:
Barney Gale 2025-02-21 17:47:45 +00:00 committed by GitHub
parent 286c517db0
commit d88677ac20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 24 additions and 18 deletions

View file

@ -192,7 +192,7 @@ class JoinablePath(ABC):
Return True if this path matches the given glob-style pattern. The Return True if this path matches the given glob-style pattern. The
pattern is matched against the entire path. pattern is matched against the entire path.
""" """
if not isinstance(pattern, JoinablePath): if not hasattr(pattern, 'with_segments'):
pattern = self.with_segments(pattern) pattern = self.with_segments(pattern)
if case_sensitive is None: if case_sensitive is None:
case_sensitive = self.parser.normcase('Aa') == 'Aa' case_sensitive = self.parser.normcase('Aa') == 'Aa'
@ -286,7 +286,7 @@ class ReadablePath(JoinablePath):
"""Iterate over this subtree and yield all existing files (of any """Iterate over this subtree and yield all existing files (of any
kind, including directories) matching the given relative pattern. kind, including directories) matching the given relative pattern.
""" """
if not isinstance(pattern, JoinablePath): if not hasattr(pattern, 'with_segments'):
pattern = self.with_segments(pattern) pattern = self.with_segments(pattern)
anchor, parts = _explode_path(pattern) anchor, parts = _explode_path(pattern)
if anchor: if anchor:
@ -348,7 +348,7 @@ class ReadablePath(JoinablePath):
""" """
Recursively copy this file or directory tree to the given destination. Recursively copy this file or directory tree to the given destination.
""" """
if not hasattr(target, '_copy_writer'): if not hasattr(target, 'with_segments'):
target = self.with_segments(target) target = self.with_segments(target)
# Delegate to the target path's CopyWriter object. # Delegate to the target path's CopyWriter object.
@ -366,7 +366,7 @@ class ReadablePath(JoinablePath):
name = self.name name = self.name
if not name: if not name:
raise ValueError(f"{self!r} has an empty name") raise ValueError(f"{self!r} has an empty name")
elif hasattr(target_dir, '_copy_writer'): elif hasattr(target_dir, 'with_segments'):
target = target_dir / name target = target_dir / name
else: else:
target = self.with_segments(target_dir, name) target = self.with_segments(target_dir, name)

View file

@ -475,7 +475,7 @@ class PurePath:
The *walk_up* parameter controls whether `..` may be used to resolve The *walk_up* parameter controls whether `..` may be used to resolve
the path. the path.
""" """
if not isinstance(other, PurePath): if not hasattr(other, 'with_segments'):
other = self.with_segments(other) other = self.with_segments(other)
for step, path in enumerate(chain([other], other.parents)): for step, path in enumerate(chain([other], other.parents)):
if path == self or path in self.parents: if path == self or path in self.parents:
@ -492,7 +492,7 @@ class PurePath:
def is_relative_to(self, other): def is_relative_to(self, other):
"""Return True if the path is relative to another path or False. """Return True if the path is relative to another path or False.
""" """
if not isinstance(other, PurePath): if not hasattr(other, 'with_segments'):
other = self.with_segments(other) other = self.with_segments(other)
return other == self or other in self.parents return other == self or other in self.parents
@ -545,7 +545,7 @@ class PurePath:
Return True if this path matches the given glob-style pattern. The Return True if this path matches the given glob-style pattern. The
pattern is matched against the entire path. pattern is matched against the entire path.
""" """
if not isinstance(pattern, PurePath): if not hasattr(pattern, 'with_segments'):
pattern = self.with_segments(pattern) pattern = self.with_segments(pattern)
if case_sensitive is None: if case_sensitive is None:
case_sensitive = self.parser is posixpath case_sensitive = self.parser is posixpath
@ -564,7 +564,7 @@ class PurePath:
is matched. The recursive wildcard '**' is *not* supported by this is matched. The recursive wildcard '**' is *not* supported by this
method. method.
""" """
if not isinstance(path_pattern, PurePath): if not hasattr(path_pattern, 'with_segments'):
path_pattern = self.with_segments(path_pattern) path_pattern = self.with_segments(path_pattern)
if case_sensitive is None: if case_sensitive is None:
case_sensitive = self.parser is posixpath case_sensitive = self.parser is posixpath
@ -1064,7 +1064,9 @@ class Path(PurePath):
Returns the new Path instance pointing to the target path. Returns the new Path instance pointing to the target path.
""" """
os.rename(self, target) os.rename(self, target)
return self.with_segments(target) if not hasattr(target, 'with_segments'):
target = self.with_segments(target)
return target
def replace(self, target): def replace(self, target):
""" """
@ -1077,7 +1079,9 @@ class Path(PurePath):
Returns the new Path instance pointing to the target path. Returns the new Path instance pointing to the target path.
""" """
os.replace(self, target) os.replace(self, target)
return self.with_segments(target) if not hasattr(target, 'with_segments'):
target = self.with_segments(target)
return target
_copy_writer = property(LocalCopyWriter) _copy_writer = property(LocalCopyWriter)
@ -1086,7 +1090,7 @@ class Path(PurePath):
""" """
Recursively copy this file or directory tree to the given destination. Recursively copy this file or directory tree to the given destination.
""" """
if not hasattr(target, '_copy_writer'): if not hasattr(target, 'with_segments'):
target = self.with_segments(target) target = self.with_segments(target)
# Delegate to the target path's CopyWriter object. # Delegate to the target path's CopyWriter object.
@ -1104,7 +1108,7 @@ class Path(PurePath):
name = self.name name = self.name
if not name: if not name:
raise ValueError(f"{self!r} has an empty name") raise ValueError(f"{self!r} has an empty name")
elif hasattr(target_dir, '_copy_writer'): elif hasattr(target_dir, 'with_segments'):
target = target_dir / name target = target_dir / name
else: else:
target = self.with_segments(target_dir, name) target = self.with_segments(target_dir, name)
@ -1118,16 +1122,13 @@ class Path(PurePath):
""" """
# Use os.replace() if the target is os.PathLike and on the same FS. # Use os.replace() if the target is os.PathLike and on the same FS.
try: try:
target_str = os.fspath(target) target = self.with_segments(target)
except TypeError: except TypeError:
pass pass
else: else:
if not hasattr(target, '_copy_writer'):
target = self.with_segments(target_str)
ensure_different_files(self, target) ensure_different_files(self, target)
try: try:
os.replace(self, target_str) return self.replace(target)
return target
except OSError as err: except OSError as err:
if err.errno != EXDEV: if err.errno != EXDEV:
raise raise
@ -1143,7 +1144,7 @@ class Path(PurePath):
name = self.name name = self.name
if not name: if not name:
raise ValueError(f"{self!r} has an empty name") raise ValueError(f"{self!r} has an empty name")
elif hasattr(target_dir, '_copy_writer'): elif hasattr(target_dir, 'with_segments'):
target = target_dir / name target = target_dir / name
else: else:
target = self.with_segments(target_dir, name) target = self.with_segments(target_dir, name)

View file

@ -0,0 +1,5 @@
Apply type conversion consistently in :class:`pathlib.PurePath` and
:class:`~pathlib.Path` methods can accept a path object as an argument, such
as :meth:`~pathlib.PurePath.match` and :meth:`~pathlib.Path.rename`. The
argument is now converted to path object if it lacks a
:meth:`~pathlib.PurePath.with_segments` attribute, and not otherwise.