From d48d7960450afd01fbccf3691b43a2b229c53b0a Mon Sep 17 00:00:00 2001 From: Nils Philippsen Date: Fri, 12 Feb 2021 11:30:53 +0100 Subject: [PATCH] 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 --- Doc/library/pathlib.rst | 17 ++++++++++++++-- Lib/pathlib.py | 18 +++++++++++++++-- Lib/test/test_pathlib.py | 20 +++++++++++++++++++ .../2020-06-25-16-34-59.bpo-34137.JomN6i.rst | 4 ++++ 4 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-06-25-16-34-59.bpo-34137.JomN6i.rst diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst index ac96de334b3290..f882983b8784ed 100644 --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -739,7 +739,7 @@ call fails (for example because the path doesn't exist). 33060 -.. method:: Path.exists() +.. method:: Path.exists(*, follow_symlinks=True) Whether the path points to an existing file or directory:: @@ -754,7 +754,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() @@ -903,6 +907,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 @@ -1219,6 +1231,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.link_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 531a699a40df49..c9ae54d8ee4000 100644 --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -1402,12 +1402,18 @@ def symlink_to(self, target, target_is_directory=False): # 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 @@ -1549,6 +1555,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 9be72941d33544..1096dd80f6935c 100644 --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -1509,11 +1509,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`.