Skip to content

GH-132566: Add pathlib.Path.info.is_junction() #132569

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions Doc/library/pathlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
4 changes: 2 additions & 2 deletions Lib/pathlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
21 changes: 20 additions & 1 deletion Lib/pathlib/_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions Lib/pathlib/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
8 changes: 7 additions & 1 deletion Lib/test/test_pathlib/support/local_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,15 @@ 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)
self._exists = None
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."""
Expand Down Expand Up @@ -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):
"""
Expand Down
3 changes: 3 additions & 0 deletions Lib/test/test_pathlib/support/zip_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions Lib/test/test_pathlib/test_pathlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
9 changes: 9 additions & 0 deletions Lib/test/test_pathlib/test_read.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Add :meth:`pathlib.types.PathInfo.is_junction` method, which returns true if
the path is a Windows junction.
Loading