Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Include/internal/pycore_exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ extern void _PyExc_Fini(PyInterpreterState *);
struct _Py_exc_state {
// The dict mapping from errno codes to OSError subclasses
PyObject *errnomap;
// Tuple of the locale's identity and its _Py_strerror() messages, or None
// if not built yet, see strerror_ensure_cache()
PyObject *strerror_cache;
PyMutex strerror_lock;
PyBaseExceptionObject *memerrors_freelist;
int memerrors_numfree;
#ifdef Py_GIL_DISABLED
Expand Down
23 changes: 23 additions & 0 deletions Lib/test/test_exception_hierarchy.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import builtins
import locale
import os
import select
import socket
import unittest
import errno
from errno import EEXIST
from test.support import cpython_only


class SubOSError(OSError):
Expand Down Expand Up @@ -136,6 +138,27 @@
if os.name == "nt":
self.assertEqual(e.winerror, None)

@cpython_only
@unittest.skipIf(os.name == "nt", "POSIX-specific test")
def test_strerror_cached(self):
def strerror():
with self.assertRaises(FileNotFoundError) as cm:
open("some_hopefully_non_existing_file")
return cm.exception.strerror
message = strerror()
self.assertIs(strerror(), message)
self.assertIs(os.strerror(errno.ENOENT), message)
if hasattr(locale, "LC_MESSAGES"):
old = locale.setlocale(locale.LC_MESSAGES)
self.addCleanup(locale.setlocale, locale.LC_MESSAGES, old)
locale.setlocale(locale.LC_MESSAGES, "C")
message = strerror()
try:
locale.setlocale(locale.LC_MESSAGES, "C.UTF-8")
self.assertIsNot(strerror(), message)

Check failure on line 158 in Lib/test/test_exception_hierarchy.py

View workflow job for this annotation

GitHub Actions / Emscripten / build and test

test_strerror_cached (test.test_exception_hierarchy.AttributesTest.test_strerror_cached)

AssertionError: unexpectedly identical: 'No such file or directory'
except locale.Error:
pass

@unittest.skipUnless(os.name == "nt", "Windows-specific test")
def test_errno_translation(self):
# ERROR_ALREADY_EXISTS (183) -> EEXIST
Expand Down
3 changes: 3 additions & 0 deletions Objects/exceptions.c
Original file line number Diff line number Diff line change
Expand Up @@ -4593,6 +4593,7 @@ PyStatus
_PyExc_InitState(PyInterpreterState *interp)
{
struct _Py_exc_state *state = &interp->exc_state;
state->strerror_cache = Py_None;

#define ADD_ERRNO(TYPE, CODE) \
do { \
Expand Down Expand Up @@ -4699,6 +4700,8 @@ _PyExc_Fini(PyInterpreterState *interp)
struct _Py_exc_state *state = &interp->exc_state;
free_preallocated_memerrors(state);
Py_CLEAR(state->errnomap);
// Not NULL: _Py_strerror() can still run after this.
Py_XSETREF(state->strerror_cache, Py_None);

_PyExc_FiniTypes(interp);
}
Expand Down
95 changes: 88 additions & 7 deletions Python/fileutils.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "Python.h"
#include "pycore_fileutils.h" // fileutils definitions
#include "pycore_long.h" // _PyLong_FromUnsignedChar()
#include "pycore_runtime.h" // _PyRuntime
#include "pycore_pystate.h" // _Py_AssertHoldsTstate()
#include "pycore_unicodeobject.h" // _Py_MAX_UNICODE
Expand Down Expand Up @@ -3304,13 +3305,8 @@ _Py_IsValidFD(int fd)
}


// Call strerror_r(code) if available, or use strerror() otherwise. Decode the
// result from the locale encoding using surrogateescape error handler.
//
// On success, return a Unicode string. On error, set an exception and return
// NULL.
PyObject*
_Py_strerror(int code)
static PyObject *
decode_strerror(int code)
/*[clinic end generated code: output=baebf09fa02a78f2 input=75a8673d97915a91]*/
{
const char *errors = "surrogateescape";
Expand Down Expand Up @@ -3396,3 +3392,88 @@ _Py_strerror(int code)
return PyUnicode_DecodeLocale(message, errors);
#endif
}


// Return a strong reference to the tuple (codeset, LC_MESSAGES name,
// {code: message}) of the current locale, replacing it if the locale changed.
static PyObject *
strerror_ensure_cache(struct _Py_exc_state *state)
{
// C code can change the locale without locale.setlocale(). Messages depend
// on LC_MESSAGES (language) and the LC_CTYPE codeset (decoding). Windows
// has neither, and its messages are ASCII.
#if defined(HAVE_LANGINFO_H) && defined(CODESET)
const char *codeset = nl_langinfo(CODESET);
const char *messages = setlocale(LC_MESSAGES, NULL);
#else
const char *codeset = "";
const char *messages = "";
#endif
// Py_SETREF() isn't atomic.
PyObject *cache;
Py_BEGIN_CRITICAL_SECTION_MUTEX(&state->strerror_lock);
cache = state->strerror_cache;
Py_IncRef(cache);
Py_END_CRITICAL_SECTION();
if (cache != Py_None
&& PyUnicode_CompareWithASCIIString(
PyTuple_GET_ITEM(cache, 0), codeset) == 0
&& PyUnicode_CompareWithASCIIString(
PyTuple_GET_ITEM(cache, 1), messages) == 0)
{
return cache;
}
Py_DECREF(cache);
cache = PyTuple_New(3);
if (cache == NULL) {
return NULL;
}
// Latin-1, which is how the comparison above reads a non-ASCII name.
PyTuple_SET_ITEM(cache, 0,
PyUnicode_DecodeLatin1(codeset, strlen(codeset), NULL));
PyTuple_SET_ITEM(cache, 1,
PyUnicode_DecodeLatin1(messages, strlen(messages), NULL));
PyTuple_SET_ITEM(cache, 2, PyDict_New());
if (PyTuple_GET_ITEM(cache, 0) == NULL
|| PyTuple_GET_ITEM(cache, 1) == NULL
|| PyTuple_GET_ITEM(cache, 2) == NULL)
{
Py_DECREF(cache);
return NULL;
}
Py_BEGIN_CRITICAL_SECTION_MUTEX(&state->strerror_lock);
Py_SETREF(state->strerror_cache, Py_NewRef(cache));
Py_END_CRITICAL_SECTION();
return cache;
}

// Call strerror_r(code) if available, or use strerror() otherwise. Decode the
// result from the locale encoding using surrogateescape error handler.
// Messages of codes 0-255 are cached per locale: decoding is a large part of
// the cost of raising OSError.
//
// On success, return a Unicode string. On error, set an exception and return
// NULL.
PyObject*
_Py_strerror(int code)
{
if (code < 0 || code >= 256) {
return decode_strerror(code);
}
struct _Py_exc_state *state = &_PyInterpreterState_GET()->exc_state;
PyObject *cache = strerror_ensure_cache(state);
if (cache == NULL) {
return NULL;
}
PyObject *dict = PyTuple_GET_ITEM(cache, 2);
PyObject *key = _PyLong_FromUnsignedChar((unsigned char)code);
PyObject *message;
if (PyDict_GetItemRef(dict, key, &message) == 0) {
message = decode_strerror(code);
if (message != NULL && PyDict_SetItem(dict, key, message) < 0) {
Py_CLEAR(message);
}
}
Py_DECREF(cache);
return message;
}
Loading