https://github.com/python/cpython/commit/d88677ac20b9466387459d5adb2e87b7de64bc19
commit: d88677ac20b9466387459d5adb2e87b7de64bc19
branch: main
author: Barney Gale <[email protected]>
committer: barneygale <[email protected]>
date: 2025-02-21T17:47:45Z
summary:

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.

files:
A Misc/NEWS.d/next/Library/2025-02-16-18-13-40.gh-issue-128520.iZtOMz.rst
M Lib/pathlib/_abc.py
M Lib/pathlib/_local.py

diff --git a/Lib/pathlib/_abc.py b/Lib/pathlib/_abc.py
index 33e963c944fc5f..115e120572d9f1 100644
--- a/Lib/pathlib/_abc.py
+++ b/Lib/pathlib/_abc.py
@@ -192,7 +192,7 @@ def full_match(self, pattern, *, case_sensitive=None):
         Return True if this path matches the given glob-style pattern. The
         pattern is matched against the entire path.
         """
-        if not isinstance(pattern, JoinablePath):
+        if not hasattr(pattern, 'with_segments'):
             pattern = self.with_segments(pattern)
         if case_sensitive is None:
             case_sensitive = self.parser.normcase('Aa') == 'Aa'
@@ -286,7 +286,7 @@ def glob(self, pattern, *, case_sensitive=None, 
recurse_symlinks=True):
         """Iterate over this subtree and yield all existing files (of any
         kind, including directories) matching the given relative pattern.
         """
-        if not isinstance(pattern, JoinablePath):
+        if not hasattr(pattern, 'with_segments'):
             pattern = self.with_segments(pattern)
         anchor, parts = _explode_path(pattern)
         if anchor:
@@ -348,7 +348,7 @@ def copy(self, target, follow_symlinks=True, 
dirs_exist_ok=False,
         """
         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)
 
         # Delegate to the target path's CopyWriter object.
@@ -366,7 +366,7 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
         name = self.name
         if not 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
         else:
             target = self.with_segments(target_dir, name)
diff --git a/Lib/pathlib/_local.py b/Lib/pathlib/_local.py
index bd1e871ebcfad7..9b2738652e9754 100644
--- a/Lib/pathlib/_local.py
+++ b/Lib/pathlib/_local.py
@@ -475,7 +475,7 @@ def relative_to(self, other, *, walk_up=False):
         The *walk_up* parameter controls whether `..` may be used to resolve
         the path.
         """
-        if not isinstance(other, PurePath):
+        if not hasattr(other, 'with_segments'):
             other = self.with_segments(other)
         for step, path in enumerate(chain([other], other.parents)):
             if path == self or path in self.parents:
@@ -492,7 +492,7 @@ def relative_to(self, other, *, walk_up=False):
     def is_relative_to(self, other):
         """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)
         return other == self or other in self.parents
 
@@ -545,7 +545,7 @@ def full_match(self, pattern, *, case_sensitive=None):
         Return True if this path matches the given glob-style pattern. The
         pattern is matched against the entire path.
         """
-        if not isinstance(pattern, PurePath):
+        if not hasattr(pattern, 'with_segments'):
             pattern = self.with_segments(pattern)
         if case_sensitive is None:
             case_sensitive = self.parser is posixpath
@@ -564,7 +564,7 @@ def match(self, path_pattern, *, case_sensitive=None):
         is matched. The recursive wildcard '**' is *not* supported by this
         method.
         """
-        if not isinstance(path_pattern, PurePath):
+        if not hasattr(path_pattern, 'with_segments'):
             path_pattern = self.with_segments(path_pattern)
         if case_sensitive is None:
             case_sensitive = self.parser is posixpath
@@ -1064,7 +1064,9 @@ def rename(self, target):
         Returns the new Path instance pointing to the target path.
         """
         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):
         """
@@ -1077,7 +1079,9 @@ def replace(self, target):
         Returns the new Path instance pointing to the target path.
         """
         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)
 
@@ -1086,7 +1090,7 @@ def copy(self, target, follow_symlinks=True, 
dirs_exist_ok=False,
         """
         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)
 
         # Delegate to the target path's CopyWriter object.
@@ -1104,7 +1108,7 @@ def copy_into(self, target_dir, *, follow_symlinks=True,
         name = self.name
         if not 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
         else:
             target = self.with_segments(target_dir, name)
@@ -1118,16 +1122,13 @@ def move(self, target):
         """
         # Use os.replace() if the target is os.PathLike and on the same FS.
         try:
-            target_str = os.fspath(target)
+            target = self.with_segments(target)
         except TypeError:
             pass
         else:
-            if not hasattr(target, '_copy_writer'):
-                target = self.with_segments(target_str)
             ensure_different_files(self, target)
             try:
-                os.replace(self, target_str)
-                return target
+                return self.replace(target)
             except OSError as err:
                 if err.errno != EXDEV:
                     raise
@@ -1143,7 +1144,7 @@ def move_into(self, target_dir):
         name = self.name
         if not 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
         else:
             target = self.with_segments(target_dir, name)
diff --git 
a/Misc/NEWS.d/next/Library/2025-02-16-18-13-40.gh-issue-128520.iZtOMz.rst 
b/Misc/NEWS.d/next/Library/2025-02-16-18-13-40.gh-issue-128520.iZtOMz.rst
new file mode 100644
index 00000000000000..15de99e8bbdbcd
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2025-02-16-18-13-40.gh-issue-128520.iZtOMz.rst
@@ -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.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3/lists/python-checkins.python.org/
Member address: [email protected]

Reply via email to