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
5 changes: 5 additions & 0 deletions Include/internal/pycore_exceptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ extern void _PyExc_Fini(PyInterpreterState *);
struct _Py_exc_state {
// The dict mapping from errno codes to OSError subclasses
PyObject *errnomap;
// _Py_strerror() messages of the current locale as the tuple
// (codeset, LC_MESSAGES, LANGUAGE, {code: message}), 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
37 changes: 37 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, is_android, os_helper


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

@cpython_only
@unittest.skipIf(os.name == "nt", "POSIX-specific test")
def test_strerror_cached(self):
message = os.strerror(errno.ENOENT)
self.assertIs(os.strerror(errno.ENOENT), message)
with self.assertRaises(FileNotFoundError) as cm:
open("some_hopefully_non_existing_file")
self.assertIs(cm.exception.strerror, message)
# Android ignores the locale (_Py_FORCE_UTF8_LOCALE).
if is_android:
return
# The locale part of the cache key. musl (and WASI) has no C.UTF-8
# LC_MESSAGES: it's "C".
def key():
return (locale.nl_langinfo(locale.CODESET),
locale.setlocale(locale.LC_MESSAGES))
for category in (locale.LC_CTYPE, locale.LC_MESSAGES):
old = locale.setlocale(category)
self.addCleanup(locale.setlocale, category, old)
locale.setlocale(category, "C")
message = os.strerror(errno.ENOENT)
old_key = key()
for name in ("C.UTF-8", "C.utf8", "en_US.UTF-8"):
try:
locale.setlocale(category, name)
except locale.Error:
continue
if key() != old_key:
self.assertIsNot(os.strerror(errno.ENOENT), message)
break
message = os.strerror(errno.ENOENT)
with os_helper.EnvironmentVarGuard() as env:
env["LANGUAGE"] = "de"
self.assertIsNot(os.strerror(errno.ENOENT), message)

@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
103 changes: 100 additions & 3 deletions Python/fileutils.c
Original file line number Diff line number Diff line change
Expand Up @@ -3309,9 +3309,8 @@ _Py_IsValidFD(int fd)
//
// On success, return a Unicode string. On error, set an exception and return
// NULL.
PyObject*
_Py_strerror(int code)
/*[clinic end generated code: output=baebf09fa02a78f2 input=75a8673d97915a91]*/
static PyObject *

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you keep the comment please?

_Py_strerror_uncached(int code)
{
const char *errors = "surrogateescape";

Expand Down Expand Up @@ -3396,3 +3395,101 @@ _Py_strerror(int code)
return PyUnicode_DecodeLocale(message, errors);
#endif
}


// Return a strong reference to the tuple (codeset, LC_MESSAGES, LANGUAGE,
// {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 calling Python locale.setlocale().
// Messages depend on LC_MESSAGES (language), the LC_CTYPE codeset
// (decoding) and, for glibc's gettext, the LANGUAGE priority list unless
// LC_MESSAGES is "C". Windows has none, and _Py_FORCE_UTF8_LOCALE ignores
// the codeset, see _Py_GetLocaleEncoding().
#if defined(_Py_FORCE_UTF8_LOCALE) || defined(MS_WINDOWS)
const char *codeset = "";
const char *messages = "";
const char *language = "";
#else
const char *codeset = nl_langinfo(CODESET);
const char *messages = setlocale(LC_MESSAGES, NULL);
const char *language = getenv("LANGUAGE");
if (codeset == NULL) {
codeset = "";
}
if (messages == NULL) {
messages = "";
}
if (language == NULL) {
language = "";
}
#endif
// Py_SETREF() isn't atomic.
PyObject *cache;
Py_BEGIN_CRITICAL_SECTION_MUTEX(&state->strerror_lock);
cache = Py_NewRef(state->strerror_cache);
Py_END_CRITICAL_SECTION();
if (cache != Py_None
&& strcmp(PyBytes_AS_STRING(PyTuple_GET_ITEM(cache, 0)), codeset) == 0
&& strcmp(PyBytes_AS_STRING(PyTuple_GET_ITEM(cache, 1)), messages) == 0
&& strcmp(PyBytes_AS_STRING(PyTuple_GET_ITEM(cache, 2)), language) == 0)
{
return cache;
}
Py_DECREF(cache);
cache = PyTuple_New(4);
if (cache == NULL) {
return NULL;
}
PyTuple_SET_ITEM(cache, 0, PyBytes_FromString(codeset));
PyTuple_SET_ITEM(cache, 1, PyBytes_FromString(messages));
PyTuple_SET_ITEM(cache, 2, PyBytes_FromString(language));
PyTuple_SET_ITEM(cache, 3, PyDict_New());
if (PyTuple_GET_ITEM(cache, 0) == NULL
|| PyTuple_GET_ITEM(cache, 1) == NULL
|| PyTuple_GET_ITEM(cache, 2) == NULL
|| PyTuple_GET_ITEM(cache, 3) == 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;
}

// os.strerror() takes any int, lets not cache INT_MAX.
#define STRERROR_CACHE_MAX 5000

// _Py_strerror_uncached() with the messages cached per locale: decoding is a

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rewrite this comment...

// large part of the cost of raising OSError.
PyObject*
_Py_strerror(int 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, 3);
PyObject *key = PyLong_FromLong(code);
if (key == NULL) {
Py_DECREF(cache);
return NULL;
}
PyObject *message;
if (PyDict_GetItemRef(dict, key, &message) == 0) {
message = _Py_strerror_uncached(code);
if (message != NULL
&& PyDict_GET_SIZE(dict) < STRERROR_CACHE_MAX
&& PyDict_SetItem(dict, key, message) < 0)
{
Py_CLEAR(message);
}
}
Py_DECREF(key);
Py_DECREF(cache);
return message;
}
Loading