Skip to content

Commit 3735ab6

Browse files
gh-109714: Pass the file name and a message when raising OSError subclasses
* Pass the file name as the filename argument instead of interpolating it into the message, so that it is available as the filename attribute: in shutil, logging.config, turtle, zipfile.Path and importlib.resources. * Give an explicit message where the one derived from errno would be misleading: "timed out" for timeouts which are not connection timeouts, and "No such resource" in importlib.resources. * In Tools, which is run with older Pythons too, pass errno explicitly. * Add _PyErr_SetOSErrorWithMessage() for raising an OSError subclass with an errno which is not the default one of the class. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent fe3a26f commit 3735ab6

17 files changed

Lines changed: 70 additions & 34 deletions

File tree

Include/internal/pycore_pyerrors.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,16 @@ PyAPI_FUNC(void) _PyErr_SetString(
136136
PyObject *exception,
137137
const char *string);
138138

139+
/*
140+
* Raise an OSError subclass with an explicit errno value, so that the
141+
* resulting exception has a meaningful errno attribute. msg is used as
142+
* strerror. Prefer PyErr_SetFromErrno() when the C errno is already set.
143+
*/
144+
PyAPI_FUNC(void) _PyErr_SetOSErrorWithMessage(
145+
PyObject *exception,
146+
int err,
147+
const char *msg);
148+
139149
/*
140150
* Set an exception with the error message decoded from the current locale
141151
* encoding (LC_CTYPE).

Lib/asyncio/tasks.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ async def wait_for(fut, timeout):
480480
try:
481481
return fut.result()
482482
except exceptions.CancelledError as exc:
483-
raise TimeoutError from exc
483+
raise TimeoutError('timed out') from exc
484484

485485
async with timeouts.timeout(timeout):
486486
return await fut
@@ -613,7 +613,7 @@ async def _wait_for_one(self, resolve=False):
613613
f = await self._done.get()
614614
if f is None:
615615
# Dummy value from _handle_timeout().
616-
raise exceptions.TimeoutError
616+
raise exceptions.TimeoutError('timed out')
617617
return f.result() if resolve else f
618618

619619

Lib/asyncio/timeouts.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ async def __aexit__(
112112
# Since there are no new cancel requests, we're
113113
# handling this.
114114
if issubclass(exc_type, exceptions.CancelledError):
115-
raise TimeoutError from exc_val
115+
raise TimeoutError('timed out') from exc_val
116116
elif exc_val is not None:
117117
self._insert_timeout_error(exc_val)
118118
if isinstance(exc_val, ExceptionGroup):
@@ -134,7 +134,7 @@ def _on_timeout(self) -> None:
134134
def _insert_timeout_error(exc_val: BaseException) -> None:
135135
while exc_val.__context__ is not None:
136136
if isinstance(exc_val.__context__, exceptions.CancelledError):
137-
te = TimeoutError()
137+
te = TimeoutError('timed out')
138138
te.__context__ = te.__cause__ = exc_val.__context__
139139
exc_val.__context__ = te
140140
break

Lib/concurrent/futures/_base.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -460,7 +460,7 @@ def result(self, timeout=None):
460460
elif self._state == FINISHED:
461461
return self.__get_result()
462462
else:
463-
raise TimeoutError()
463+
raise TimeoutError('timed out')
464464
finally:
465465
# Break a reference cycle with the exception in self._exception
466466
self = None
@@ -496,7 +496,7 @@ def exception(self, timeout=None):
496496
elif self._state == FINISHED:
497497
return self._exception
498498
else:
499-
raise TimeoutError()
499+
raise TimeoutError('timed out')
500500

501501
# The following methods should only be used by Executors and in tests.
502502
def set_running_or_notify_cancel(self):

Lib/importlib/resources/abc.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def open_resource(self, resource: Text) -> BinaryIO:
3434
# This deliberately raises FileNotFoundError instead of
3535
# NotImplementedError so that if this method is accidentally called,
3636
# it'll still do the right thing.
37-
raise FileNotFoundError
37+
raise FileNotFoundError('No such resource')
3838

3939
@abc.abstractmethod
4040
def resource_path(self, resource: Text) -> Text:
@@ -47,20 +47,20 @@ def resource_path(self, resource: Text) -> Text:
4747
# This deliberately raises FileNotFoundError instead of
4848
# NotImplementedError so that if this method is accidentally called,
4949
# it'll still do the right thing.
50-
raise FileNotFoundError
50+
raise FileNotFoundError('No such resource')
5151

5252
@abc.abstractmethod
5353
def is_resource(self, path: Text) -> bool:
5454
"""Return True if the named 'path' is a resource.
5555
5656
Files are resources, directories are not.
5757
"""
58-
raise FileNotFoundError
58+
raise FileNotFoundError('No such resource')
5959

6060
@abc.abstractmethod
6161
def contents(self) -> Iterable[str]:
6262
"""Return an iterable of entries in `package`."""
63-
raise FileNotFoundError
63+
raise FileNotFoundError('No such resource')
6464

6565

6666
class TraversalError(Exception):
@@ -180,7 +180,7 @@ def open_resource(self, resource: StrPath) -> BinaryIO:
180180
return self.files().joinpath(resource).open('rb')
181181

182182
def resource_path(self, resource: Any) -> NoReturn:
183-
raise FileNotFoundError(resource)
183+
raise FileNotFoundError('No such resource', filename=resource)
184184

185185
def is_resource(self, path: StrPath) -> bool:
186186
return self.files().joinpath(path).is_file()

Lib/importlib/resources/readers.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,10 @@ def open_resource(self, resource):
4646
try:
4747
return super().open_resource(resource)
4848
except KeyError as exc:
49-
raise FileNotFoundError(exc.args[0])
49+
if resource == exc.args[0]:
50+
raise FileNotFoundError('No such resource', filename=resource)
51+
else:
52+
raise FileNotFoundError(exc.args[0])
5053

5154
def is_resource(self, path):
5255
"""
@@ -73,8 +76,10 @@ def __init__(self, *paths):
7376
if not self._paths:
7477
message = 'MultiplexedPath must contain at least one path'
7578
raise FileNotFoundError(message)
76-
if not all(path.is_dir() for path in self._paths):
77-
raise NotADirectoryError('MultiplexedPath only supports directories')
79+
for path in self._paths:
80+
if not path.is_dir():
81+
message = 'MultiplexedPath only supports directories'
82+
raise NotADirectoryError(message, filename=path)
7883

7984
def iterdir(self):
8085
children = (child for path in self._paths for child in path.iterdir())

Lib/logging/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def fileConfig(fname, defaults=None, disable_existing_loggers=True, encoding=Non
6565

6666
if isinstance(fname, str):
6767
if not os.path.exists(fname):
68-
raise FileNotFoundError(f"{fname} doesn't exist")
68+
raise FileNotFoundError('No such file', filename=fname)
6969
elif not os.path.getsize(fname):
7070
raise RuntimeError(f'{fname} is an empty file')
7171

Lib/multiprocessing/pool.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -792,7 +792,7 @@ def wait(self, timeout=None):
792792
def get(self, timeout=None):
793793
self.wait(timeout)
794794
if not self.ready():
795-
raise TimeoutError
795+
raise TimeoutError('timed out')
796796
if self._success:
797797
return self._value
798798
else:
@@ -893,7 +893,7 @@ def next(self, timeout=None):
893893
except IndexError:
894894
if self._index == self._length:
895895
self._stop_iterator()
896-
raise TimeoutError from None
896+
raise TimeoutError('timed out') from None
897897

898898
if self._buffersize_sema is not None:
899899
self._buffersize_sema.release()

Lib/shutil.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,8 @@ def copyfile(src, dst, *, follow_symlinks=True):
362362
# Issue 43219, raise a less confusing exception
363363
except IsADirectoryError as e:
364364
if not os.path.exists(dst):
365-
raise FileNotFoundError(f'Directory does not exist: {dst}') from e
365+
raise FileNotFoundError('Directory does not exist',
366+
filename=dst) from e
366367
else:
367368
raise
368369

@@ -948,9 +949,8 @@ def move(src, dst, copy_function=copy2):
948949
if (_is_immutable(src)
949950
or (not os.access(src, os.W_OK) and os.listdir(src)
950951
and sys.platform == 'darwin')):
951-
raise PermissionError("Cannot move the non-empty directory "
952-
"'%s': Lacking write permission to '%s'."
953-
% (src, src))
952+
raise PermissionError("Cannot move the non-empty directory: "
953+
"Lacking write permission", filename=src)
954954
copytree(src, real_dst, copy_function=copy_function,
955955
symlinks=True)
956956
rmtree(src)

Lib/test/test_turtle.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -510,7 +510,8 @@ def test_save_raises_if_parent_not_found(self) -> None:
510510

511511
with tempfile.TemporaryDirectory() as tmpdir:
512512
parent = os.path.join(tmpdir, "unknown_parent")
513-
msg = f"The directory '{parent}' does not exist. Cannot save to it"
513+
msg = ("The directory does not exist. Cannot save to it: "
514+
f"'{parent}'")
514515

515516
with self.assertRaisesRegex(FileNotFoundError, re.escape(msg)):
516517
turtle.TurtleScreen.save(screen, os.path.join(parent, "a.ps"))
@@ -524,8 +525,9 @@ def test_save_raises_if_file_found(self) -> None:
524525
f.write("some text")
525526

526527
msg = (
527-
f"The file '{file_path}' already exists. To overwrite it use"
528-
" the 'overwrite=True' argument of the save function."
528+
"The file already exists. To overwrite it use"
529+
" the 'overwrite=True' argument of the save function: "
530+
f"'{file_path}'"
529531
)
530532
with self.assertRaisesRegex(FileExistsError, re.escape(msg)):
531533
turtle.TurtleScreen.save(screen, file_path)

0 commit comments

Comments
 (0)