diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index c5cf409f5f1f81..2fb8c6b92b2556 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -818,7 +818,7 @@ call fails (for example because the path doesn't exist). .. versionchanged:: 3.10 The *follow_symlinks* parameter was added. -.. method:: Path.exists() +.. method:: Path.exists(*, follow_symlinks=True) Whether the path points to an existing file or directory:: @@ -833,7 +833,11 @@ call fails (for example because the path doesn't exist). .. note:: If the path points to a symlink, :meth:`exists` returns whether the - symlink *points to* an existing file or directory. + symlink *points to* an existing file or directory, unless + *follow_symlinks* is False. + + .. versionchanged:: 3.10 + The *follow_symlinks* parameter was added. .. method:: Path.expanduser() @@ -1097,6 +1101,14 @@ call fails (for example because the path doesn't exist). symbolic link's mode is changed rather than its target's. +.. method:: Path.lexists() + + Like :meth:`Path.exists` but, if the path points to a symbolic link, return + the symbolic link's information rather than its target's. + + .. versionadded:: 3.10 + + .. method:: Path.lstat() Like :meth:`Path.stat` but, if the path points to a symbolic link, return @@ -1436,6 +1448,7 @@ Below is a table mapping various :mod:`os` functions to their corresponding :func:`os.path.isdir` :meth:`Path.is_dir` :func:`os.path.isfile` :meth:`Path.is_file` :func:`os.path.islink` :meth:`Path.is_symlink` +:func:`os.path.lexists` :meth:`Path.lexists` :func:`os.link` :meth:`Path.hardlink_to` :func:`os.symlink` :meth:`Path.symlink_to` :func:`os.readlink` :meth:`Path.readlink` diff --git a/Lib/pathlib.py b/Lib/pathlib.py index a0678f61b63211..7af60cd20d57ff 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1061,12 +1061,18 @@ def hardlink_to(self, target): # Convenience functions for querying the stat results - def exists(self): + def exists(self, *, follow_symlinks=True): """ Whether this path exists. + + Returns False for broken symbolic links unless follow_symlinks is set + to False. """ try: - self.stat() + if follow_symlinks: + self.stat() + else: + self.lstat() except OSError as e: if not _ignore_error(e): raise @@ -1200,6 +1206,14 @@ def is_socket(self): # Non-encodable path return False + def lexists(self): + """ + Whether this path exists, but don't follow symbolic links. + + Returns True for broken symbolic links. + """ + return self.exists(follow_symlinks=False) + def expanduser(self): """ Return a new path with expanded ~ and ~user constructs (as returned by os.path.expanduser) diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py index 7d4d782cf5f075..b3b70261b86e80 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1646,11 +1646,31 @@ def test_exists(self): self.assertIs(True, (p / 'linkB').exists()) self.assertIs(True, (p / 'linkB' / 'fileB').exists()) self.assertIs(False, (p / 'linkA' / 'bah').exists()) + self.assertIs(False, (p / 'brokenLink').exists()) + self.assertIs(False, (p / 'brokenLinkLoop').exists()) + self.assertIs(True, (p / 'brokenLink').exists( + follow_symlinks=False)) + self.assertIs(True, (p / 'brokenLinkLoop').exists( + follow_symlinks=False)) self.assertIs(False, (p / 'foo').exists()) self.assertIs(False, P('/xyzzy').exists()) self.assertIs(False, P(BASE + '\udfff').exists()) self.assertIs(False, P(BASE + '\x00').exists()) + @os_helper.skip_unless_symlink + def test_exists_follow_symlinks(self): + P = self.cls + p = P(BASE) + self.assertIs(True, (p / 'brokenLink').exists(follow_symlinks=False)) + self.assertIs(True, (p / 'brokenLinkLoop').exists(follow_symlinks=False)) + + @os_helper.skip_unless_symlink + def test_lexists(self): + P = self.cls + p = P(BASE) + self.assertIs(True, (p / 'brokenLink').lexists()) + self.assertIs(True, (p / 'brokenLinkLoop').lexists()) + def test_open_common(self): p = self.cls(BASE) with (p / 'fileA').open('r') as f: diff --git a/Misc/NEWS.d/next/Library/2020-06-25-16-34-59.bpo-34137.JomN6i.rst b/Misc/NEWS.d/next/Library/2020-06-25-16-34-59.bpo-34137.JomN6i.rst new file mode 100644 index 00000000000000..b93bf50b8b6c58 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-06-25-16-34-59.bpo-34137.JomN6i.rst @@ -0,0 +1,4 @@ +Add the *follow_symlinks* parameter in :meth:`pathlib.Path.exists` and +:meth:`pathlib.Path.lexists` as a wrapper analogous to +:func:`os.stat` ./. :func:`os.lstat` and +:func:`os.path.exists` ./. :func:`os.path.lexists`.