diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index b82986902861b2..da78a9306cd443 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -1210,9 +1210,10 @@ Querying file type and status any filesystem queries. To fetch up-to-date information, it's best to call :meth:`Path.is_dir`, - :meth:`~Path.is_file` and :meth:`~Path.is_symlink` rather than methods of - this attribute. There is no way to reset the cache; instead you can create - a new path object with an empty info cache via ``p = Path(p)``. + :meth:`~Path.is_file`, :meth:`~Path.is_symlink`, and + :meth:`~Path.is_junction` rather than methods of this attribute. There is + no way to reset the cache; instead you can create a new path object with an + empty info cache via ``p = Path(p)``. .. versionadded:: 3.14 @@ -1989,3 +1990,8 @@ The :mod:`pathlib.types` module provides types for static type checking. Return ``True`` if the path is a symbolic link (even if broken); return ``False`` if the path is a directory or any kind of file, or if it doesn't exist. + + .. method:: is_junction() + + Return ``True`` if the path is a junction; return ``False`` for any + other type of file. Currently only Windows supports junctions. diff --git a/Lib/pathlib/__init__.py b/Lib/pathlib/__init__.py index 12cf9f579cb32d..9aafd1a537f5ec 100644 --- a/Lib/pathlib/__init__.py +++ b/Lib/pathlib/__init__.py @@ -1059,9 +1059,9 @@ def _delete(self): """ Delete this file or directory (including all sub-directories). """ - if self.is_symlink() or self.is_junction(): + if self.info.is_symlink() or self.info.is_junction(): self.unlink() - elif self.is_dir(): + elif self.info.is_dir(): # Lazy import to improve module import time import shutil shutil.rmtree(self) diff --git a/Lib/pathlib/_os.py b/Lib/pathlib/_os.py index e3751bbcb62377..9fcc253e55fb91 100644 --- a/Lib/pathlib/_os.py +++ b/Lib/pathlib/_os.py @@ -390,7 +390,7 @@ def _xattrs(self, *, follow_symlinks=True): class _WindowsPathInfo(_PathInfoBase): """Implementation of pathlib.types.PathInfo that provides status information for Windows paths. Don't try to construct it yourself.""" - __slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink') + __slots__ = ('_exists', '_is_dir', '_is_file', '_is_symlink', '_is_junction') def exists(self, *, follow_symlinks=True): """Whether this path exists.""" @@ -442,6 +442,14 @@ def is_symlink(self): self._is_symlink = os.path.islink(self._path) return self._is_symlink + def is_junction(self): + """Whether this path is a junction.""" + try: + return self._is_junction + except AttributeError: + self._is_junction = os.path.isjunction(self._path) + return self._is_junction + class _PosixPathInfo(_PathInfoBase): """Implementation of pathlib.types.PathInfo that provides status @@ -476,6 +484,10 @@ def is_symlink(self): return False return S_ISLNK(st.st_mode) + def is_junction(self): + """Whether this path is a junction.""" + return False + PathInfo = _WindowsPathInfo if os.name == 'nt' else _PosixPathInfo @@ -524,3 +536,10 @@ def is_symlink(self): return self._entry.is_symlink() except OSError: return False + + def is_junction(self): + """Whether this path is a junction.""" + try: + return self._entry.is_junction() + except OSError: + return False diff --git a/Lib/pathlib/types.py b/Lib/pathlib/types.py index d1bb8701b887c8..260b201a3af27c 100644 --- a/Lib/pathlib/types.py +++ b/Lib/pathlib/types.py @@ -57,6 +57,7 @@ def exists(self, *, follow_symlinks: bool = True) -> bool: ... def is_dir(self, *, follow_symlinks: bool = True) -> bool: ... def is_file(self, *, follow_symlinks: bool = True) -> bool: ... def is_symlink(self) -> bool: ... + def is_junction(self) -> bool: ... class _JoinablePath(ABC): diff --git a/Lib/test/test_pathlib/support/local_path.py b/Lib/test/test_pathlib/support/local_path.py index 4f027754f6a6e1..ee2fc0716b7a84 100644 --- a/Lib/test/test_pathlib/support/local_path.py +++ b/Lib/test/test_pathlib/support/local_path.py @@ -94,7 +94,7 @@ class LocalPathInfo(PathInfo): """ Simple implementation of PathInfo for a local path """ - __slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink') + __slots__ = ('_path', '_exists', '_is_dir', '_is_file', '_is_symlink', '_is_junction') def __init__(self, path): self._path = str(path) @@ -102,6 +102,7 @@ def __init__(self, path): self._is_dir = None self._is_file = None self._is_symlink = None + self._is_junction = None def exists(self, *, follow_symlinks=True): """Whether this path exists.""" @@ -133,6 +134,11 @@ def is_symlink(self): self._is_symlink = os.path.islink(self._path) return self._is_symlink + def is_junction(self): + if self._is_junction is None: + self._is_junction = os.path.isjunction(self._path) + return self._is_junction + class ReadableLocalPath(_ReadablePath, LexicalPath): """ diff --git a/Lib/test/test_pathlib/support/zip_path.py b/Lib/test/test_pathlib/support/zip_path.py index 242cab1509627b..585bdb138cf698 100644 --- a/Lib/test/test_pathlib/support/zip_path.py +++ b/Lib/test/test_pathlib/support/zip_path.py @@ -107,6 +107,9 @@ def is_file(self, follow_symlinks=True): def is_symlink(self): return False + def is_junction(self): + return False + def resolve(self): return self diff --git a/Lib/test/test_pathlib/test_pathlib.py b/Lib/test/test_pathlib/test_pathlib.py index 41a79d0dceb0eb..1f9b8ddc16d4c8 100644 --- a/Lib/test/test_pathlib/test_pathlib.py +++ b/Lib/test/test_pathlib/test_pathlib.py @@ -2557,6 +2557,13 @@ def test_info_is_symlink_caching(self): q.unlink() self.assertTrue(q.info.is_symlink()) + @needs_windows + def test_info_is_junction(self): + p = self.cls(self.base, 'fileA') + with mock.patch.object(os.path, 'isjunction'): + self.assertFalse(p.info.is_junction()) + os.path.isjunction.assert_called_once_with(p) + def test_stat(self): statA = self.cls(self.base).joinpath('fileA').stat() statB = self.cls(self.base).joinpath('dirB', 'fileB').stat() diff --git a/Lib/test/test_pathlib/test_read.py b/Lib/test/test_pathlib/test_read.py index 753ae5d760aceb..1661c5ad177456 100644 --- a/Lib/test/test_pathlib/test_read.py +++ b/Lib/test/test_pathlib/test_read.py @@ -301,6 +301,15 @@ def test_info_is_symlink(self): self.assertFalse((p / 'fileA\udfff').info.is_symlink()) self.assertFalse((p / 'fileA\x00').info.is_symlink()) + def test_info_is_junction(self): + p = self.root + self.assertFalse((p / 'fileA').info.is_junction()) + self.assertFalse((p / 'dirA').info.is_junction()) + self.assertFalse((p / 'non-existing').info.is_junction()) + self.assertFalse((p / 'fileA' / 'bah').info.is_junction()) + self.assertFalse((p / 'fileA\udfff').info.is_junction()) + self.assertFalse((p / 'fileA\x00').info.is_junction()) + class ZipPathReadTest(ReadTestBase, unittest.TestCase): ground = ZipPathGround(ReadableZipPath) diff --git a/Misc/NEWS.d/next/Library/2025-04-15-20-30-05.gh-issue-132566.IMeNM5.rst b/Misc/NEWS.d/next/Library/2025-04-15-20-30-05.gh-issue-132566.IMeNM5.rst new file mode 100644 index 00000000000000..ca233282ab5d50 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-04-15-20-30-05.gh-issue-132566.IMeNM5.rst @@ -0,0 +1,2 @@ +Add :meth:`pathlib.types.PathInfo.is_junction` method, which returns true if +the path is a Windows junction.