Repository navigation
gh-161242: Cache strerror() messages per errno #161455
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
98e6d6e
1629843
53d9206
ffcbf0f
98bfddf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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): | ||
|
|
@@ -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 | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
| except locale.Error: | ||
| pass | ||
|
|
||
| @unittest.skipUnless(os.name == "nt", "Windows-specific test") | ||
| def test_errno_translation(self): | ||
| # ERROR_ALREADY_EXISTS (183) -> EEXIST | ||
|
|
||
| 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 | ||||||||
|
|
@@ -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 * | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you keep the comment please? |
||||||||
| decode_strerror(int code) | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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]*/ | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This clinic line can now be removed. |
||||||||
| { | ||||||||
| const char *errors = "surrogateescape"; | ||||||||
|
|
@@ -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 | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| // 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) | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. According to _Py_GetLocaleEncoding(), Python doesn't both with checking |
||||||||
| const char *codeset = nl_langinfo(CODESET); | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||||||||
| } | ||||||||
There was a problem hiding this comment.
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}).