Skip to content

Commit

Permalink
GH-105793: Add follow_symlinks argument to pathlib.Path.is_dir() an…
Browse files Browse the repository at this point in the history
…d `is_file()` (GH-105794)

Brings `pathlib.Path.is_dir()` and `in line with `os.DirEntry.is_dir()`, which
will be important for implementing generic path walking and globbing.
Likewise `is_file()`.
  • Loading branch information
barneygale committed Jun 26, 2023
1 parent 5d4dbf0 commit 219effa
Show file tree
Hide file tree
Showing 5 changed files with 59 additions and 18 deletions.
24 changes: 18 additions & 6 deletions Doc/library/pathlib.rst
Expand Up @@ -978,23 +978,35 @@ call fails (for example because the path doesn't exist).
available. In previous versions, :exc:`NotImplementedError` was raised.


.. method:: Path.is_dir()
.. method:: Path.is_dir(*, follow_symlinks=True)

Return ``True`` if the path points to a directory (or a symbolic link
pointing to a directory), ``False`` if it points to another kind of file.
Return ``True`` if the path points to a directory, ``False`` if it points
to another kind of file.

``False`` is also returned if the path doesn't exist or is a broken symlink;
other errors (such as permission errors) are propagated.

This method normally follows symlinks; to exclude symlinks to directories,
add the argument ``follow_symlinks=False``.

.. method:: Path.is_file()
.. versionchanged:: 3.13
The *follow_symlinks* parameter was added.


.. method:: Path.is_file(*, follow_symlinks=True)

Return ``True`` if the path points to a regular file (or a symbolic link
pointing to a regular file), ``False`` if it points to another kind of file.
Return ``True`` if the path points to a regular file, ``False`` if it
points to another kind of file.

``False`` is also returned if the path doesn't exist or is a broken symlink;
other errors (such as permission errors) are propagated.

This method normally follows symlinks; to exclude symlinks, add the
argument ``follow_symlinks=False``.

.. versionchanged:: 3.13
The *follow_symlinks* parameter was added.


.. method:: Path.is_junction()

Expand Down
7 changes: 4 additions & 3 deletions Doc/whatsnew/3.13.rst
Expand Up @@ -113,9 +113,10 @@ pathlib
* Add support for recursive wildcards in :meth:`pathlib.PurePath.match`.
(Contributed by Barney Gale in :gh:`73435`.)

* Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.glob` and
:meth:`~pathlib.Path.rglob`.
(Contributed by Barney Gale in :gh:`77609`.)
* Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.glob`,
:meth:`~pathlib.Path.rglob`, :meth:`~pathlib.Path.is_file`, and
:meth:`~pathlib.Path.is_dir`.
(Contributed by Barney Gale in :gh:`77609` and :gh:`105793`.)

traceback
---------
Expand Down
8 changes: 4 additions & 4 deletions Lib/pathlib.py
Expand Up @@ -817,12 +817,12 @@ def exists(self, *, follow_symlinks=True):
return False
return True

def is_dir(self):
def is_dir(self, *, follow_symlinks=True):
"""
Whether this path is a directory.
"""
try:
return S_ISDIR(self.stat().st_mode)
return S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode)
except OSError as e:
if not _ignore_error(e):
raise
Expand All @@ -833,13 +833,13 @@ def is_dir(self):
# Non-encodable path
return False

def is_file(self):
def is_file(self, *, follow_symlinks=True):
"""
Whether this path is a regular file (also True for symlinks pointing
to regular files).
"""
try:
return S_ISREG(self.stat().st_mode)
return S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode)
except OSError as e:
if not _ignore_error(e):
raise
Expand Down
36 changes: 31 additions & 5 deletions Lib/test/test_pathlib.py
Expand Up @@ -2191,9 +2191,22 @@ def test_is_dir(self):
if os_helper.can_symlink():
self.assertFalse((P / 'linkA').is_dir())
self.assertTrue((P / 'linkB').is_dir())
self.assertFalse((P/ 'brokenLink').is_dir(), False)
self.assertIs((P / 'dirA\udfff').is_dir(), False)
self.assertIs((P / 'dirA\x00').is_dir(), False)
self.assertFalse((P/ 'brokenLink').is_dir())
self.assertFalse((P / 'dirA\udfff').is_dir())
self.assertFalse((P / 'dirA\x00').is_dir())

def test_is_dir_no_follow_symlinks(self):
P = self.cls(BASE)
self.assertTrue((P / 'dirA').is_dir(follow_symlinks=False))
self.assertFalse((P / 'fileA').is_dir(follow_symlinks=False))
self.assertFalse((P / 'non-existing').is_dir(follow_symlinks=False))
self.assertFalse((P / 'fileA' / 'bah').is_dir(follow_symlinks=False))
if os_helper.can_symlink():
self.assertFalse((P / 'linkA').is_dir(follow_symlinks=False))
self.assertFalse((P / 'linkB').is_dir(follow_symlinks=False))
self.assertFalse((P/ 'brokenLink').is_dir(follow_symlinks=False))
self.assertFalse((P / 'dirA\udfff').is_dir(follow_symlinks=False))
self.assertFalse((P / 'dirA\x00').is_dir(follow_symlinks=False))

def test_is_file(self):
P = self.cls(BASE)
Expand All @@ -2205,8 +2218,21 @@ def test_is_file(self):
self.assertTrue((P / 'linkA').is_file())
self.assertFalse((P / 'linkB').is_file())
self.assertFalse((P/ 'brokenLink').is_file())
self.assertIs((P / 'fileA\udfff').is_file(), False)
self.assertIs((P / 'fileA\x00').is_file(), False)
self.assertFalse((P / 'fileA\udfff').is_file())
self.assertFalse((P / 'fileA\x00').is_file())

def test_is_file_no_follow_symlinks(self):
P = self.cls(BASE)
self.assertTrue((P / 'fileA').is_file(follow_symlinks=False))
self.assertFalse((P / 'dirA').is_file(follow_symlinks=False))
self.assertFalse((P / 'non-existing').is_file(follow_symlinks=False))
self.assertFalse((P / 'fileA' / 'bah').is_file(follow_symlinks=False))
if os_helper.can_symlink():
self.assertFalse((P / 'linkA').is_file(follow_symlinks=False))
self.assertFalse((P / 'linkB').is_file(follow_symlinks=False))
self.assertFalse((P/ 'brokenLink').is_file(follow_symlinks=False))
self.assertFalse((P / 'fileA\udfff').is_file(follow_symlinks=False))
self.assertFalse((P / 'fileA\x00').is_file(follow_symlinks=False))

def test_is_mount(self):
P = self.cls(BASE)
Expand Down
@@ -0,0 +1,2 @@
Add *follow_symlinks* keyword-only argument to :meth:`pathlib.Path.is_dir` and
:meth:`~pathlib.Path.is_file`, defaulting to ``True``.

0 comments on commit 219effa

Please sign in to comment.