Skip to content

Commit d48d796

Browse files
committed
bpo-34137: Add pathlib.Path.lexists and related
This adds the `follow_symlink` parameter to `pathlib.Path.exists()`, and wraps the new functionality in `pathlib.Path.lexists()`, analogous to `os.stat()` ./. `os.lstat()` and `os.path.exists()` ./. `os.path.lexists()`. GH-NNNN Signed-off-by: Nils Philippsen <[email protected]>
1 parent 2068b26 commit d48d796

File tree

4 files changed

+55
-4
lines changed

4 files changed

+55
-4
lines changed

Doc/library/pathlib.rst

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -739,7 +739,7 @@ call fails (for example because the path doesn't exist).
739739
33060
740740

741741

742-
.. method:: Path.exists()
742+
.. method:: Path.exists(*, follow_symlinks=True)
743743

744744
Whether the path points to an existing file or directory::
745745

@@ -754,7 +754,11 @@ call fails (for example because the path doesn't exist).
754754

755755
.. note::
756756
If the path points to a symlink, :meth:`exists` returns whether the
757-
symlink *points to* an existing file or directory.
757+
symlink *points to* an existing file or directory, unless
758+
*follow_symlinks* is False.
759+
760+
.. versionchanged:: 3.10
761+
The *follow_symlinks* parameter was added.
758762

759763

760764
.. method:: Path.expanduser()
@@ -903,6 +907,14 @@ call fails (for example because the path doesn't exist).
903907
symbolic link's mode is changed rather than its target's.
904908

905909

910+
.. method:: Path.lexists()
911+
912+
Like :meth:`Path.exists` but, if the path points to a symbolic link, return
913+
the symbolic link's information rather than its target's.
914+
915+
.. versionadded:: 3.10
916+
917+
906918
.. method:: Path.lstat()
907919

908920
Like :meth:`Path.stat` but, if the path points to a symbolic link, return
@@ -1219,6 +1231,7 @@ Below is a table mapping various :mod:`os` functions to their corresponding
12191231
:func:`os.path.isdir` :meth:`Path.is_dir`
12201232
:func:`os.path.isfile` :meth:`Path.is_file`
12211233
:func:`os.path.islink` :meth:`Path.is_symlink`
1234+
:func:`os.path.lexists` :meth:`Path.lexists`
12221235
:func:`os.link` :meth:`Path.link_to`
12231236
:func:`os.symlink` :meth:`Path.symlink_to`
12241237
:func:`os.readlink` :meth:`Path.readlink`

Lib/pathlib.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,12 +1402,18 @@ def symlink_to(self, target, target_is_directory=False):
14021402

14031403
# Convenience functions for querying the stat results
14041404

1405-
def exists(self):
1405+
def exists(self, *, follow_symlinks=True):
14061406
"""
14071407
Whether this path exists.
1408+
1409+
Returns False for broken symbolic links unless follow_symlinks is set
1410+
to False.
14081411
"""
14091412
try:
1410-
self.stat()
1413+
if follow_symlinks:
1414+
self.stat()
1415+
else:
1416+
self.lstat()
14111417
except OSError as e:
14121418
if not _ignore_error(e):
14131419
raise
@@ -1549,6 +1555,14 @@ def is_socket(self):
15491555
# Non-encodable path
15501556
return False
15511557

1558+
def lexists(self):
1559+
"""
1560+
Whether this path exists, but don't follow symbolic links.
1561+
1562+
Returns True for broken symbolic links.
1563+
"""
1564+
return self.exists(follow_symlinks=False)
1565+
15521566
def expanduser(self):
15531567
""" Return a new path with expanded ~ and ~user constructs
15541568
(as returned by os.path.expanduser)

Lib/test/test_pathlib.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,11 +1509,31 @@ def test_exists(self):
15091509
self.assertIs(True, (p / 'linkB').exists())
15101510
self.assertIs(True, (p / 'linkB' / 'fileB').exists())
15111511
self.assertIs(False, (p / 'linkA' / 'bah').exists())
1512+
self.assertIs(False, (p / 'brokenLink').exists())
1513+
self.assertIs(False, (p / 'brokenLinkLoop').exists())
1514+
self.assertIs(True, (p / 'brokenLink').exists(
1515+
follow_symlinks=False))
1516+
self.assertIs(True, (p / 'brokenLinkLoop').exists(
1517+
follow_symlinks=False))
15121518
self.assertIs(False, (p / 'foo').exists())
15131519
self.assertIs(False, P('/xyzzy').exists())
15141520
self.assertIs(False, P(BASE + '\udfff').exists())
15151521
self.assertIs(False, P(BASE + '\x00').exists())
15161522

1523+
@os_helper.skip_unless_symlink
1524+
def test_exists_follow_symlinks(self):
1525+
P = self.cls
1526+
p = P(BASE)
1527+
self.assertIs(True, (p / 'brokenLink').exists(follow_symlinks=False))
1528+
self.assertIs(True, (p / 'brokenLinkLoop').exists(follow_symlinks=False))
1529+
1530+
@os_helper.skip_unless_symlink
1531+
def test_lexists(self):
1532+
P = self.cls
1533+
p = P(BASE)
1534+
self.assertIs(True, (p / 'brokenLink').lexists())
1535+
self.assertIs(True, (p / 'brokenLinkLoop').lexists())
1536+
15171537
def test_open_common(self):
15181538
p = self.cls(BASE)
15191539
with (p / 'fileA').open('r') as f:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Add the *follow_symlinks* parameter in :meth:`pathlib.Path.exists` and
2+
:meth:`pathlib.Path.lexists` as a wrapper analogous to
3+
:func:`os.stat` ./. :func:`os.lstat` and
4+
:func:`os.path.exists` ./. :func:`os.path.lexists`.

0 commit comments

Comments
 (0)