GH-101357: Suppress OSError from pathlib.Path.exists() and is_*() (#118243)

Suppress all `OSError` exceptions from `pathlib.Path.exists()` and `is_*()`
rather than a selection of more common errors as we do presently. Also
adjust the implementations to call `os.path.exists()` etc, which are much
faster on Windows thanks to GH-101196.
This commit is contained in:
Barney Gale 2024-05-14 18:53:15 +01:00 committed by GitHub
parent d8e0e00919
commit fbe6a0988f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 92 additions and 125 deletions

View file

@ -873,7 +873,7 @@ Methods
^^^^^^^ ^^^^^^^
Concrete paths provide the following methods in addition to pure paths Concrete paths provide the following methods in addition to pure paths
methods. Many of these methods can raise an :exc:`OSError` if a system methods. Some of these methods can raise an :exc:`OSError` if a system
call fails (for example because the path doesn't exist). call fails (for example because the path doesn't exist).
.. versionchanged:: 3.8 .. versionchanged:: 3.8
@ -885,6 +885,15 @@ call fails (for example because the path doesn't exist).
instead of raising an exception for paths that contain characters instead of raising an exception for paths that contain characters
unrepresentable at the OS level. unrepresentable at the OS level.
.. versionchanged:: 3.14
The methods given above now return ``False`` instead of raising any
:exc:`OSError` exception from the operating system. In previous versions,
some kinds of :exc:`OSError` exception are raised, and others suppressed.
The new behaviour is consistent with :func:`os.path.exists`,
:func:`os.path.isdir`, etc. Use :meth:`~Path.stat` to retrieve the file
status without suppressing exceptions.
.. classmethod:: Path.cwd() .. classmethod:: Path.cwd()
@ -951,6 +960,8 @@ call fails (for example because the path doesn't exist).
.. method:: Path.exists(*, follow_symlinks=True) .. method:: Path.exists(*, follow_symlinks=True)
Return ``True`` if the path points to an existing file or directory. Return ``True`` if the path points to an existing file or directory.
``False`` will be returned if the path is invalid, inaccessible or missing.
Use :meth:`Path.stat` to distinguish between these cases.
This method normally follows symlinks; to check if a symlink exists, add This method normally follows symlinks; to check if a symlink exists, add
the argument ``follow_symlinks=False``. the argument ``follow_symlinks=False``.
@ -1067,11 +1078,10 @@ call fails (for example because the path doesn't exist).
.. method:: Path.is_dir(*, follow_symlinks=True) .. method:: Path.is_dir(*, follow_symlinks=True)
Return ``True`` if the path points to a directory, ``False`` if it points Return ``True`` if the path points to a directory. ``False`` will be
to another kind of file. returned if the path is invalid, inaccessible or missing, or if it points
to something other than a directory. Use :meth:`Path.stat` to distinguish
``False`` is also returned if the path doesn't exist or is a broken symlink; between these cases.
other errors (such as permission errors) are propagated.
This method normally follows symlinks; to exclude symlinks to directories, This method normally follows symlinks; to exclude symlinks to directories,
add the argument ``follow_symlinks=False``. add the argument ``follow_symlinks=False``.
@ -1082,11 +1092,10 @@ call fails (for example because the path doesn't exist).
.. method:: Path.is_file(*, follow_symlinks=True) .. method:: Path.is_file(*, follow_symlinks=True)
Return ``True`` if the path points to a regular file, ``False`` if it Return ``True`` if the path points to a regular file. ``False`` will be
points to another kind of file. returned if the path is invalid, inaccessible or missing, or if it points
to something other than a regular file. Use :meth:`Path.stat` to
``False`` is also returned if the path doesn't exist or is a broken symlink; distinguish between these cases.
other errors (such as permission errors) are propagated.
This method normally follows symlinks; to exclude symlinks, add the This method normally follows symlinks; to exclude symlinks, add the
argument ``follow_symlinks=False``. argument ``follow_symlinks=False``.
@ -1122,46 +1131,42 @@ call fails (for example because the path doesn't exist).
.. method:: Path.is_symlink() .. method:: Path.is_symlink()
Return ``True`` if the path points to a symbolic link, ``False`` otherwise. Return ``True`` if the path points to a symbolic link, even if that symlink
is broken. ``False`` will be returned if the path is invalid, inaccessible
``False`` is also returned if the path doesn't exist; other errors (such or missing, or if it points to something other than a symbolic link. Use
as permission errors) are propagated. :meth:`Path.stat` to distinguish between these cases.
.. method:: Path.is_socket() .. method:: Path.is_socket()
Return ``True`` if the path points to a Unix socket (or a symbolic link Return ``True`` if the path points to a Unix socket. ``False`` will be
pointing to a Unix socket), ``False`` if it points to another kind of file. returned if the path is invalid, inaccessible or missing, or if it points
to something other than a Unix socket. Use :meth:`Path.stat` to
``False`` is also returned if the path doesn't exist or is a broken symlink; distinguish between these cases.
other errors (such as permission errors) are propagated.
.. method:: Path.is_fifo() .. method:: Path.is_fifo()
Return ``True`` if the path points to a FIFO (or a symbolic link Return ``True`` if the path points to a FIFO. ``False`` will be returned if
pointing to a FIFO), ``False`` if it points to another kind of file. the path is invalid, inaccessible or missing, or if it points to something
other than a FIFO. Use :meth:`Path.stat` to distinguish between these
``False`` is also returned if the path doesn't exist or is a broken symlink; cases.
other errors (such as permission errors) are propagated.
.. method:: Path.is_block_device() .. method:: Path.is_block_device()
Return ``True`` if the path points to a block device (or a symbolic link Return ``True`` if the path points to a block device. ``False`` will be
pointing to a block device), ``False`` if it points to another kind of file. returned if the path is invalid, inaccessible or missing, or if it points
to something other than a block device. Use :meth:`Path.stat` to
``False`` is also returned if the path doesn't exist or is a broken symlink; distinguish between these cases.
other errors (such as permission errors) are propagated.
.. method:: Path.is_char_device() .. method:: Path.is_char_device()
Return ``True`` if the path points to a character device (or a symbolic link Return ``True`` if the path points to a character device. ``False`` will be
pointing to a character device), ``False`` if it points to another kind of file. returned if the path is invalid, inaccessible or missing, or if it points
to something other than a character device. Use :meth:`Path.stat` to
``False`` is also returned if the path doesn't exist or is a broken symlink; distinguish between these cases.
other errors (such as permission errors) are propagated.
.. method:: Path.iterdir() .. method:: Path.iterdir()

View file

@ -340,7 +340,7 @@ class _Globber:
# Low-level methods # Low-level methods
lstat = operator.methodcaller('lstat') lexists = operator.methodcaller('exists', follow_symlinks=False)
add_slash = operator.methodcaller('joinpath', '') add_slash = operator.methodcaller('joinpath', '')
@staticmethod @staticmethod
@ -516,12 +516,8 @@ class _Globber:
# Optimization: this path is already known to exist, e.g. because # Optimization: this path is already known to exist, e.g. because
# it was returned from os.scandir(), so we skip calling lstat(). # it was returned from os.scandir(), so we skip calling lstat().
yield path yield path
else: elif self.lexists(path):
try:
self.lstat(path)
yield path yield path
except OSError:
pass
@classmethod @classmethod
def walk(cls, root, top_down, on_error, follow_symlinks): def walk(cls, root, top_down, on_error, follow_symlinks):
@ -562,7 +558,7 @@ class _Globber:
class _StringGlobber(_Globber): class _StringGlobber(_Globber):
lstat = staticmethod(os.lstat) lexists = staticmethod(os.path.lexists)
scandir = staticmethod(os.scandir) scandir = staticmethod(os.scandir)
parse_entry = operator.attrgetter('path') parse_entry = operator.attrgetter('path')
concat_path = operator.add concat_path = operator.add

View file

@ -13,32 +13,12 @@ resemble pathlib's PurePath and Path respectively.
import functools import functools
from glob import _Globber, _no_recurse_symlinks from glob import _Globber, _no_recurse_symlinks
from errno import ENOENT, ENOTDIR, EBADF, ELOOP, EINVAL from errno import ENOTDIR, ELOOP
from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO from stat import S_ISDIR, S_ISLNK, S_ISREG, S_ISSOCK, S_ISBLK, S_ISCHR, S_ISFIFO
__all__ = ["UnsupportedOperation"] __all__ = ["UnsupportedOperation"]
#
# Internals
#
_WINERROR_NOT_READY = 21 # drive exists but is not accessible
_WINERROR_INVALID_NAME = 123 # fix for bpo-35306
_WINERROR_CANT_RESOLVE_FILENAME = 1921 # broken symlink pointing to itself
# EBADF - guard against macOS `stat` throwing EBADF
_IGNORED_ERRNOS = (ENOENT, ENOTDIR, EBADF, ELOOP)
_IGNORED_WINERRORS = (
_WINERROR_NOT_READY,
_WINERROR_INVALID_NAME,
_WINERROR_CANT_RESOLVE_FILENAME)
def _ignore_error(exception):
return (getattr(exception, 'errno', None) in _IGNORED_ERRNOS or
getattr(exception, 'winerror', None) in _IGNORED_WINERRORS)
@functools.cache @functools.cache
def _is_case_sensitive(parser): def _is_case_sensitive(parser):
@ -450,12 +430,7 @@ class PathBase(PurePathBase):
""" """
try: try:
self.stat(follow_symlinks=follow_symlinks) self.stat(follow_symlinks=follow_symlinks)
except OSError as e: except (OSError, ValueError):
if not _ignore_error(e):
raise
return False
except ValueError:
# Non-encodable path
return False return False
return True return True
@ -465,14 +440,7 @@ class PathBase(PurePathBase):
""" """
try: try:
return S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode) return S_ISDIR(self.stat(follow_symlinks=follow_symlinks).st_mode)
except OSError as e: except (OSError, ValueError):
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False return False
def is_file(self, *, follow_symlinks=True): def is_file(self, *, follow_symlinks=True):
@ -482,14 +450,7 @@ class PathBase(PurePathBase):
""" """
try: try:
return S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode) return S_ISREG(self.stat(follow_symlinks=follow_symlinks).st_mode)
except OSError as e: except (OSError, ValueError):
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False return False
def is_mount(self): def is_mount(self):
@ -518,13 +479,7 @@ class PathBase(PurePathBase):
""" """
try: try:
return S_ISLNK(self.lstat().st_mode) return S_ISLNK(self.lstat().st_mode)
except OSError as e: except (OSError, ValueError):
if not _ignore_error(e):
raise
# Path doesn't exist
return False
except ValueError:
# Non-encodable path
return False return False
def is_junction(self): def is_junction(self):
@ -542,14 +497,7 @@ class PathBase(PurePathBase):
""" """
try: try:
return S_ISBLK(self.stat().st_mode) return S_ISBLK(self.stat().st_mode)
except OSError as e: except (OSError, ValueError):
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False return False
def is_char_device(self): def is_char_device(self):
@ -558,14 +506,7 @@ class PathBase(PurePathBase):
""" """
try: try:
return S_ISCHR(self.stat().st_mode) return S_ISCHR(self.stat().st_mode)
except OSError as e: except (OSError, ValueError):
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False return False
def is_fifo(self): def is_fifo(self):
@ -574,14 +515,7 @@ class PathBase(PurePathBase):
""" """
try: try:
return S_ISFIFO(self.stat().st_mode) return S_ISFIFO(self.stat().st_mode)
except OSError as e: except (OSError, ValueError):
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False return False
def is_socket(self): def is_socket(self):
@ -590,14 +524,7 @@ class PathBase(PurePathBase):
""" """
try: try:
return S_ISSOCK(self.stat().st_mode) return S_ISSOCK(self.stat().st_mode)
except OSError as e: except (OSError, ValueError):
if not _ignore_error(e):
raise
# Path doesn't exist or is a broken symlink
# (see http://web.archive.org/web/20200623061726/https://bitbucket.org/pitrou/pathlib/issues/12/ )
return False
except ValueError:
# Non-encodable path
return False return False
def samefile(self, other_path): def samefile(self, other_path):

View file

@ -502,12 +502,46 @@ class Path(PathBase, PurePath):
""" """
return os.stat(self, follow_symlinks=follow_symlinks) return os.stat(self, follow_symlinks=follow_symlinks)
def exists(self, *, follow_symlinks=True):
"""
Whether this path exists.
This method normally follows symlinks; to check whether a symlink exists,
add the argument follow_symlinks=False.
"""
if follow_symlinks:
return os.path.exists(self)
return os.path.lexists(self)
def is_dir(self, *, follow_symlinks=True):
"""
Whether this path is a directory.
"""
if follow_symlinks:
return os.path.isdir(self)
return PathBase.is_dir(self, follow_symlinks=follow_symlinks)
def is_file(self, *, follow_symlinks=True):
"""
Whether this path is a regular file (also True for symlinks pointing
to regular files).
"""
if follow_symlinks:
return os.path.isfile(self)
return PathBase.is_file(self, follow_symlinks=follow_symlinks)
def is_mount(self): def is_mount(self):
""" """
Check if this path is a mount point Check if this path is a mount point
""" """
return os.path.ismount(self) return os.path.ismount(self)
def is_symlink(self):
"""
Whether this path is a symbolic link.
"""
return os.path.islink(self)
def is_junction(self): def is_junction(self):
""" """
Whether this path is a junction. Whether this path is a junction.

View file

@ -0,0 +1,5 @@
Suppress all :exc:`OSError` exceptions from :meth:`pathlib.Path.exists` and
``is_*()`` methods, rather than a selection of more common errors. The new
behaviour is consistent with :func:`os.path.exists`, :func:`os.path.isdir`,
etc. Use :meth:`Path.stat` to retrieve the file status without suppressing
exceptions.