From 9f418cd6106540e19d1477e1f7b94bc475a168e1 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Fri, 22 Sep 2023 13:09:01 +0300 Subject: [PATCH] WIP: Always pass errno argument for OSError subclasses --- Lib/asyncio/base_subprocess.py | 4 ++- Lib/asyncio/proactor_events.py | 3 +- Lib/asyncio/sslproto.py | 6 ++-- Lib/asyncio/streams.py | 3 +- Lib/asyncio/tasks.py | 6 ++-- Lib/asyncio/timeouts.py | 3 +- Lib/asyncio/unix_events.py | 2 +- Lib/concurrent/futures/_base.py | 7 +++-- Lib/importlib/resources/_adapters.py | 3 +- Lib/importlib/resources/abc.py | 3 +- Lib/importlib/resources/readers.py | 20 ++++++++----- Lib/importlib/resources/simple.py | 4 ++- Lib/logging/config.py | 2 +- Lib/multiprocessing/heap.py | 3 +- Lib/multiprocessing/pool.py | 5 ++-- Lib/multiprocessing/synchronize.py | 3 +- Lib/py_compile.py | 5 ++-- Lib/shutil.py | 8 +++--- Lib/socket.py | 2 +- Lib/subprocess.py | 2 +- .../test_as_completed.py | 3 +- Lib/zipfile/_path/__init__.py | 6 ++-- Modules/_ssl.c | 28 +++++++++++-------- Modules/socketmodule.c | 9 ++++-- 24 files changed, 88 insertions(+), 52 deletions(-) diff --git a/Lib/asyncio/base_subprocess.py b/Lib/asyncio/base_subprocess.py index 4c9b0dd5653c0c..a39d5f3ed08da5 100644 --- a/Lib/asyncio/base_subprocess.py +++ b/Lib/asyncio/base_subprocess.py @@ -1,4 +1,6 @@ import collections +import errno +import os import subprocess import warnings @@ -139,7 +141,7 @@ def get_pipe_transport(self, fd): def _check_proc(self): if self._proc is None: - raise ProcessLookupError() + raise ProcessLookupError(errno.ESRCH, os.strerror(errno.ESRCH)) def send_signal(self, signal): self._check_proc() diff --git a/Lib/asyncio/proactor_events.py b/Lib/asyncio/proactor_events.py index 1e2a730cf368a9..b79c7d45f70e89 100644 --- a/Lib/asyncio/proactor_events.py +++ b/Lib/asyncio/proactor_events.py @@ -6,6 +6,7 @@ __all__ = 'BaseProactorEventLoop', +import errno import io import os import socket @@ -452,7 +453,7 @@ def _pipe_closed(self, fut): assert fut is self._read_fut, (fut, self._read_fut) self._read_fut = None if self._write_fut is not None: - self._force_close(BrokenPipeError()) + self._force_close(BrokenPipeError(errno.EPIPE, os.strerror(errno.EPIPE))) else: self.close() diff --git a/Lib/asyncio/sslproto.py b/Lib/asyncio/sslproto.py index 3eb65a8a08b5a0..204680b5276f23 100644 --- a/Lib/asyncio/sslproto.py +++ b/Lib/asyncio/sslproto.py @@ -1,5 +1,7 @@ import collections import enum +import errno +import os import warnings try: import ssl @@ -458,7 +460,7 @@ def eof_received(self): logger.debug("%r received EOF", self) if self._state == SSLProtocolState.DO_HANDSHAKE: - self._on_handshake_complete(ConnectionResetError) + self._on_handshake_complete(ConnectionResetError(errno.ECONNRESET, os.strerror(errno.ECONNRESET))) elif self._state == SSLProtocolState.WRAPPED: self._set_state(SSLProtocolState.FLUSHING) @@ -550,7 +552,7 @@ def _check_handshake_timeout(self): f"{self._ssl_handshake_timeout} seconds: " f"aborting the connection" ) - self._fatal_error(ConnectionAbortedError(msg)) + self._fatal_error(ConnectionAbortedError(errno.ECONNABORTED, msg)) def _do_handshake(self): try: diff --git a/Lib/asyncio/streams.py b/Lib/asyncio/streams.py index b7ad365709b19e..929d295c44de91 100644 --- a/Lib/asyncio/streams.py +++ b/Lib/asyncio/streams.py @@ -3,6 +3,7 @@ 'open_connection', 'start_server') import collections +import errno import socket import sys import warnings @@ -164,7 +165,7 @@ def connection_lost(self, exc): async def _drain_helper(self): if self._connection_lost: - raise ConnectionResetError('Connection lost') + raise ConnectionResetError(errno.ECONNRESET, 'Connection lost') if not self._paused: return waiter = self._loop.create_future() diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py index edc64fda2a6ad6..d5e80a0987b398 100644 --- a/Lib/asyncio/tasks.py +++ b/Lib/asyncio/tasks.py @@ -12,10 +12,12 @@ import concurrent.futures import contextvars +import errno import functools import inspect import itertools import math +import os import types import warnings import weakref @@ -492,7 +494,7 @@ async def wait_for(fut, timeout): try: return fut.result() except exceptions.CancelledError as exc: - raise TimeoutError from exc + raise TimeoutError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) from exc async with timeouts.timeout(timeout): return await fut @@ -605,7 +607,7 @@ async def _wait_for_one(): f = await done.get() if f is None: # Dummy value from _on_timeout(). - raise exceptions.TimeoutError + raise exceptions.TimeoutError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) return f.result() # May raise f.exception(). for f in todo: diff --git a/Lib/asyncio/timeouts.py b/Lib/asyncio/timeouts.py index 029c468739bf2d..175ec03f199fac 100644 --- a/Lib/asyncio/timeouts.py +++ b/Lib/asyncio/timeouts.py @@ -1,4 +1,5 @@ import enum +import errno from types import TracebackType from typing import final, Optional, Type @@ -108,7 +109,7 @@ async def __aexit__( if self._task.uncancel() <= self._cancelling and exc_type is exceptions.CancelledError: # Since there are no new cancel requests, we're # handling this. - raise TimeoutError from exc_val + raise TimeoutError(errno.ETIMEDOUT, 'timed out') from exc_val elif self._state is _State.ENTERED: self._state = _State.EXITED diff --git a/Lib/asyncio/unix_events.py b/Lib/asyncio/unix_events.py index 28cef964debd36..4555a96213719c 100644 --- a/Lib/asyncio/unix_events.py +++ b/Lib/asyncio/unix_events.py @@ -676,7 +676,7 @@ def _read_ready(self): if self._loop.get_debug(): logger.info("%r was closed by peer", self) if self._buffer: - self._close(BrokenPipeError()) + self._close(BrokenPipeError(errno.EPIPE, os.strerror(errno.EPIPE))) else: self._close() diff --git a/Lib/concurrent/futures/_base.py b/Lib/concurrent/futures/_base.py index 6742a07753c921..38f4ae69e14da0 100644 --- a/Lib/concurrent/futures/_base.py +++ b/Lib/concurrent/futures/_base.py @@ -4,7 +4,9 @@ __author__ = 'Brian Quinlan (brian@sweetapp.com)' import collections +import errno import logging +import os import threading import time import types @@ -237,6 +239,7 @@ def as_completed(fs, timeout=None): wait_timeout = end_time - time.monotonic() if wait_timeout < 0: raise TimeoutError( + errno.ETIMEDOUT, '%d (of %d) futures unfinished' % ( len(pending), total_futures)) @@ -455,7 +458,7 @@ def result(self, timeout=None): elif self._state == FINISHED: return self.__get_result() else: - raise TimeoutError() + raise TimeoutError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) finally: # Break a reference cycle with the exception in self._exception self = None @@ -491,7 +494,7 @@ def exception(self, timeout=None): elif self._state == FINISHED: return self._exception else: - raise TimeoutError() + raise TimeoutError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) # The following methods should only be used by Executors and in tests. def set_running_or_notify_cancel(self): diff --git a/Lib/importlib/resources/_adapters.py b/Lib/importlib/resources/_adapters.py index 50688fbb666658..99e0271273d252 100644 --- a/Lib/importlib/resources/_adapters.py +++ b/Lib/importlib/resources/_adapters.py @@ -1,3 +1,4 @@ +import errno from contextlib import suppress from io import TextIOWrapper @@ -136,7 +137,7 @@ def name(self): return self._path[-1] def open(self, mode='r', *args, **kwargs): - raise FileNotFoundError("Can't open orphan path") + raise FileNotFoundError(errno.ENOENT, "Can't open orphan path") def __init__(self, spec): self.spec = spec diff --git a/Lib/importlib/resources/abc.py b/Lib/importlib/resources/abc.py index 6750a7aaf14aa9..c1504a20653ca0 100644 --- a/Lib/importlib/resources/abc.py +++ b/Lib/importlib/resources/abc.py @@ -1,4 +1,5 @@ import abc +import errno import io import itertools import os @@ -164,7 +165,7 @@ def open_resource(self, resource: StrPath) -> io.BufferedReader: return self.files().joinpath(resource).open('rb') def resource_path(self, resource: Any) -> NoReturn: - raise FileNotFoundError(resource) + raise FileNotFoundError(errno.ENOENT, 'No such resource', resource) def is_resource(self, path: StrPath) -> bool: return self.files().joinpath(path).is_file() diff --git a/Lib/importlib/resources/readers.py b/Lib/importlib/resources/readers.py index c3cdf769cbecb0..054ae81706ba8f 100644 --- a/Lib/importlib/resources/readers.py +++ b/Lib/importlib/resources/readers.py @@ -1,4 +1,5 @@ import collections +import errno import itertools import pathlib import operator @@ -39,7 +40,10 @@ def open_resource(self, resource): try: return super().open_resource(resource) except KeyError as exc: - raise FileNotFoundError(exc.args[0]) + if resource == exc.args[0]: + raise FileNotFoundError(errno.ENOENT, 'No such resource', resource) + else: + raise FileNotFoundError(errno.ENOENT, exc.args[0]) def is_resource(self, path): """ @@ -65,9 +69,11 @@ def __init__(self, *paths): self._paths = list(map(pathlib.Path, remove_duplicates(paths))) if not self._paths: message = 'MultiplexedPath must contain at least one path' - raise FileNotFoundError(message) - if not all(path.is_dir() for path in self._paths): - raise NotADirectoryError('MultiplexedPath only supports directories') + raise FileNotFoundError(errno.ENOENT, message) + for path in self._paths: + if not path.is_dir(): + message = 'MultiplexedPath only supports directories' + raise NotADirectoryError(errno.ENOTDIR, message, path) def iterdir(self): children = (child for path in self._paths for child in path.iterdir()) @@ -76,10 +82,10 @@ def iterdir(self): return map(self._follow, (locs for name, locs in groups)) def read_bytes(self): - raise FileNotFoundError(f'{self} is not a file') + raise FileNotFoundError(errno.ENOENT, f'{self} is not a file') def read_text(self, *args, **kwargs): - raise FileNotFoundError(f'{self} is not a file') + raise FileNotFoundError(errno.ENOENT, f'{self} is not a file') def is_dir(self): return True @@ -115,7 +121,7 @@ def _follow(cls, children): return next(one_file) def open(self, *args, **kwargs): - raise FileNotFoundError(f'{self} is not a file') + raise FileNotFoundError(errno.ENOENT, f'{self} is not a file') @property def name(self): diff --git a/Lib/importlib/resources/simple.py b/Lib/importlib/resources/simple.py index 7770c922c84fab..027f8a2fd5f6df 100644 --- a/Lib/importlib/resources/simple.py +++ b/Lib/importlib/resources/simple.py @@ -3,8 +3,10 @@ """ import abc +import errno import io import itertools +import os from typing import BinaryIO, List from .abc import Traversable, TraversableResources @@ -67,7 +69,7 @@ def iterdir(self): return itertools.chain(files, dirs) def open(self, *args, **kwargs): - raise IsADirectoryError() + raise IsADirectoryError(errno.EISDIR, os.strerror(errno.EISDIR)) class ResourceHandle(Traversable): diff --git a/Lib/logging/config.py b/Lib/logging/config.py index 41283f4d627267..0a9c60b664d65d 100644 --- a/Lib/logging/config.py +++ b/Lib/logging/config.py @@ -63,7 +63,7 @@ def fileConfig(fname, defaults=None, disable_existing_loggers=True, encoding=Non if isinstance(fname, str): if not os.path.exists(fname): - raise FileNotFoundError(f"{fname} doesn't exist") + raise FileNotFoundError(errno.ENOENT, 'No such file', fname) elif not os.path.getsize(fname): raise RuntimeError(f'{fname} is an empty file') diff --git a/Lib/multiprocessing/heap.py b/Lib/multiprocessing/heap.py index 6217dfe12689b3..a276bb4575848a 100644 --- a/Lib/multiprocessing/heap.py +++ b/Lib/multiprocessing/heap.py @@ -9,6 +9,7 @@ import bisect from collections import defaultdict +import errno import mmap import os import sys @@ -45,7 +46,7 @@ def __init__(self, size): # We have reopened a preexisting mmap. buf.close() else: - raise FileExistsError('Cannot find name for new mmap') + raise FileExistsError(errno.EEXIST, 'Cannot find name for new mmap') self.name = name self.buffer = buf self._state = (self.size, self.name) diff --git a/Lib/multiprocessing/pool.py b/Lib/multiprocessing/pool.py index 4f5d88cb975cb7..4859edaa7fcdda 100644 --- a/Lib/multiprocessing/pool.py +++ b/Lib/multiprocessing/pool.py @@ -14,6 +14,7 @@ # import collections +import errno import itertools import os import queue @@ -767,7 +768,7 @@ def wait(self, timeout=None): def get(self, timeout=None): self.wait(timeout) if not self.ready(): - raise TimeoutError + raise TimeoutError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) if self._success: return self._value else: @@ -865,7 +866,7 @@ def next(self, timeout=None): if self._index == self._length: self._pool = None raise StopIteration from None - raise TimeoutError from None + raise TimeoutError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) from None success, value = item if success: diff --git a/Lib/multiprocessing/synchronize.py b/Lib/multiprocessing/synchronize.py index 3ccbfe311c71f3..0025e22c9a6845 100644 --- a/Lib/multiprocessing/synchronize.py +++ b/Lib/multiprocessing/synchronize.py @@ -11,6 +11,7 @@ 'Lock', 'RLock', 'Semaphore', 'BoundedSemaphore', 'Condition', 'Event' ] +import errno import threading import sys import tempfile @@ -62,7 +63,7 @@ def __init__(self, kind, value, maxvalue, *, ctx): else: break else: - raise FileExistsError('cannot find name for semaphore') + raise FileExistsError(errno.EEXIST, 'cannot find name for semaphore') util.debug('created semlock with handle %s' % sl.handle) self._make_methods() diff --git a/Lib/py_compile.py b/Lib/py_compile.py index 388614e51b1847..1e2737ef7bbd69 100644 --- a/Lib/py_compile.py +++ b/Lib/py_compile.py @@ -4,6 +4,7 @@ """ import enum +import errno import importlib._bootstrap_external import importlib.machinery import importlib.util @@ -133,11 +134,11 @@ def compile(file, cfile=None, dfile=None, doraise=False, optimize=-1, if os.path.islink(cfile): msg = ('{} is a symlink and will be changed into a regular file if ' 'import writes a byte-compiled file to it') - raise FileExistsError(msg.format(cfile)) + raise FileExistsError(errno.EEXIST, msg.format(cfile)) elif os.path.exists(cfile) and not os.path.isfile(cfile): msg = ('{} is a non-regular file and will be changed into a regular ' 'one if import writes a byte-compiled file to it') - raise FileExistsError(msg.format(cfile)) + raise FileExistsError(errno.EEXIST, msg.format(cfile)) loader = importlib.machinery.SourceFileLoader('', file) source_bytes = loader.get_data(file) try: diff --git a/Lib/shutil.py b/Lib/shutil.py index b37bd082eee0c6..4de8ca13c45252 100644 --- a/Lib/shutil.py +++ b/Lib/shutil.py @@ -285,7 +285,7 @@ def copyfile(src, dst, *, follow_symlinks=True): # Issue 43219, raise a less confusing exception except IsADirectoryError as e: if not os.path.exists(dst): - raise FileNotFoundError(f'Directory does not exist: {dst}') from e + raise FileNotFoundError(errno.ENOENT, 'Directory does not exist', dst) from e else: raise @@ -872,9 +872,9 @@ def move(src, dst, copy_function=copy2): if (_is_immutable(src) or (not os.access(src, os.W_OK) and os.listdir(src) and sys.platform == 'darwin')): - raise PermissionError("Cannot move the non-empty directory " - "'%s': Lacking write permission to '%s'." - % (src, src)) + raise PermissionError(errno.EACCES, + "Cannot move the non-empty directory: " + "Lacking write permission", src) copytree(src, real_dst, copy_function=copy_function, symlinks=True) rmtree(src) diff --git a/Lib/socket.py b/Lib/socket.py index 321fcda51505c1..5b794a749bfb86 100644 --- a/Lib/socket.py +++ b/Lib/socket.py @@ -380,7 +380,7 @@ def _sendfile_use_sendfile(self, file, offset=0, count=None): try: while True: if timeout and not selector_select(timeout): - raise TimeoutError('timed out') + raise TimeoutError(errno.ETIMEDOUT, os.strerror(errno.ETIMEDOUT)) if count: blocksize = count - total_sent if blocksize <= 0: diff --git a/Lib/subprocess.py b/Lib/subprocess.py index 6df5dd551ea67e..4458e21ab7bc31 100644 --- a/Lib/subprocess.py +++ b/Lib/subprocess.py @@ -1520,7 +1520,7 @@ def _execute_child(self, args, executable, preexec_fn, close_fds, system_root = os.environ.get('SystemRoot', '') comspec = os.path.join(system_root, 'System32', 'cmd.exe') if not os.path.isabs(comspec): - raise FileNotFoundError('shell not found: neither %ComSpec% nor %SystemRoot% is set') + raise FileNotFoundError(errno.ENOENT, 'shell not found: neither %ComSpec% nor %SystemRoot% is set') if os.path.isabs(comspec): executable = comspec else: diff --git a/Lib/test/test_concurrent_futures/test_as_completed.py b/Lib/test/test_concurrent_futures/test_as_completed.py index 2b3bec8cafbcb0..5cb81cf7afe8c6 100644 --- a/Lib/test/test_concurrent_futures/test_as_completed.py +++ b/Lib/test/test_concurrent_futures/test_as_completed.py @@ -1,3 +1,4 @@ +import errno import itertools import time import unittest @@ -101,7 +102,7 @@ def test_correct_timeout_exception_msg(self): with self.assertRaises(futures.TimeoutError) as cm: list(futures.as_completed(futures_list, timeout=0)) - self.assertEqual(str(cm.exception), '2 (of 4) futures unfinished') + self.assertEqual(str(cm.exception), f'[Errno {errno.ETIMEDOUT}] 2 (of 4) futures unfinished') create_executor_tests(globals(), AsCompletedTests) diff --git a/Lib/zipfile/_path/__init__.py b/Lib/zipfile/_path/__init__.py index 78c413563bb2b1..5c63fd263f3754 100644 --- a/Lib/zipfile/_path/__init__.py +++ b/Lib/zipfile/_path/__init__.py @@ -1,4 +1,6 @@ +import errno import io +import os import posixpath import zipfile import itertools @@ -284,10 +286,10 @@ def open(self, mode='r', *args, pwd=None, **kwargs): to io.TextIOWrapper(). """ if self.is_dir(): - raise IsADirectoryError(self) + raise IsADirectoryError(errno.EISDIR, os.strerror(errno.EISDIR), self) zip_mode = mode[0] if not self.exists() and zip_mode == 'r': - raise FileNotFoundError(self) + raise FileNotFoundError(errno.ENOENT, 'No such file', self) stream = self.root.open(self.at, zip_mode, pwd=pwd) if 'b' in mode: if args or kwargs: diff --git a/Modules/_ssl.c b/Modules/_ssl.c index cecc3785c7661c..94ac23be7f37c1 100644 --- a/Modules/_ssl.c +++ b/Modules/_ssl.c @@ -436,6 +436,16 @@ PyDoc_STRVAR(SSLSyscallError_doc, PyDoc_STRVAR(SSLEOFError_doc, "SSL/TLS connection terminated abruptly."); +static void +set_timeout_err(const char *errmsg) +{ + PyObject *exc = PyObject_CallFunction(PyExc_TimeoutError, "is", ETIMEDOUT, errmsg); + if (exc) { + PyErr_SetObject(PyExc_TimeoutError, exc); + Py_DECREF(exc); + } +} + static PyObject * SSLError_str(PyOSErrorObject *self) { @@ -1009,8 +1019,7 @@ _ssl__SSLSocket_do_handshake_impl(PySSLSocket *self) } if (sockstate == SOCKET_HAS_TIMED_OUT) { - PyErr_SetString(PyExc_TimeoutError, - ERRSTR("The handshake operation timed out")); + set_timeout_err(ERRSTR("The handshake operation timed out")); goto error; } else if (sockstate == SOCKET_HAS_BEEN_CLOSED) { PyErr_SetString(get_state_sock(self)->PySSLErrorObject, @@ -2383,8 +2392,7 @@ _ssl__SSLSocket_write_impl(PySSLSocket *self, Py_buffer *b) sockstate = PySSL_select(sock, 1, timeout); if (sockstate == SOCKET_HAS_TIMED_OUT) { - PyErr_SetString(PyExc_TimeoutError, - "The write operation timed out"); + set_timeout_err("The write operation timed out"); goto error; } else if (sockstate == SOCKET_HAS_BEEN_CLOSED) { PyErr_SetString(get_state_sock(self)->PySSLErrorObject, @@ -2419,8 +2427,7 @@ _ssl__SSLSocket_write_impl(PySSLSocket *self, Py_buffer *b) } if (sockstate == SOCKET_HAS_TIMED_OUT) { - PyErr_SetString(PyExc_TimeoutError, - "The write operation timed out"); + set_timeout_err("The write operation timed out"); goto error; } else if (sockstate == SOCKET_HAS_BEEN_CLOSED) { PyErr_SetString(get_state_sock(self)->PySSLErrorObject, @@ -2577,8 +2584,7 @@ _ssl__SSLSocket_read_impl(PySSLSocket *self, Py_ssize_t len, sockstate = SOCKET_OPERATION_OK; if (sockstate == SOCKET_HAS_TIMED_OUT) { - PyErr_SetString(PyExc_TimeoutError, - "The read operation timed out"); + set_timeout_err("The read operation timed out"); goto error; } else if (sockstate == SOCKET_IS_NONBLOCKING) { break; @@ -2695,11 +2701,9 @@ _ssl__SSLSocket_shutdown_impl(PySSLSocket *self) if (sockstate == SOCKET_HAS_TIMED_OUT) { if (err.ssl == SSL_ERROR_WANT_READ) - PyErr_SetString(PyExc_TimeoutError, - "The read operation timed out"); + set_timeout_err("The read operation timed out"); else - PyErr_SetString(PyExc_TimeoutError, - "The write operation timed out"); + set_timeout_err("The write operation timed out"); goto error; } else if (sockstate == SOCKET_TOO_LARGE_FOR_SELECT) { diff --git a/Modules/socketmodule.c b/Modules/socketmodule.c index 90592ffc152fc1..aaa0de24e60e19 100644 --- a/Modules/socketmodule.c +++ b/Modules/socketmodule.c @@ -959,8 +959,10 @@ sock_call_ex(PySocketSockObject *s, if (res == 1) { if (err) *err = SOCK_TIMEOUT_ERR; - else - PyErr_SetString(PyExc_TimeoutError, "timed out"); + else { + errno = ETIMEDOUT; + PyErr_SetFromErrno(PyExc_TimeoutError); + } return -1; } @@ -4416,7 +4418,8 @@ sock_sendall(PySocketSockObject *s, PyObject *args) } if (timeout <= 0) { - PyErr_SetString(PyExc_TimeoutError, "timed out"); + errno = ETIMEDOUT; + PyErr_SetFromErrno(PyExc_TimeoutError); goto done; } }