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()

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.

You may describe the format here: (codeset, LC_MESSAGES, {code: message}).

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

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.

Why not testing os.sterror() here?

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")

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.

You may try more locales since locale names change a lot depending on the operating system (and also between Linux distributions? not sure). For example, on my Fedora 45 this locale is called "C.utf8". See maybe test_locale and test__locale for locale names.

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 *

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?

decode_strerror(int code)

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.

I would prefer to call it _Py_strerror_impl() or _Py_strerror_uncached().

/*[clinic end generated code: output=baebf09fa02a78f2 input=75a8673d97915a91]*/

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.

This clinic line can now be removed.

{
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

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.

Suggested change
// C code can change the locale without locale.setlocale(). Messages depend
// C code can change the locale without calling Python 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)

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.

According to _Py_GetLocaleEncoding(), Python doesn't both with checking defined(HAVE_LANGINFO_H) && defined(CODESET): it just uses it in all platforms but Windows. So #ifdef MS_WINDOWS should be enough.

const char *codeset = nl_langinfo(CODESET);

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.

You should look _Py_GetLocaleEncoding() for a more complete (correct) implementation: need to check _Py_FORCE_UTF8_LOCALE and use "utf-8" if nl_langinfo() returns NULL or an empty string.

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);
Comment on lines +3415 to +3416

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.

Suggested change
cache = state->strerror_cache;
Py_IncRef(cache);
cache = Py_NewRef(state->strerror_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));
Comment on lines +3432 to +3435

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.

If you use latin1, you don't really care of the encoding. In this case, why not using bytes which may be a little bit faster? PyBytes_FromStringAndSize().

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) {

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.

Is it really need to exclude some codes? Maybe just remove that, no?

The result is not cached if decode_strerror() fails anyway.

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