Skip to content

gh-138004: fix threadmodule ascii and make thread naming test more lenient #138017

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 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
19 changes: 15 additions & 4 deletions Lib/test/test_threading.py
Original file line number Diff line number Diff line change
Expand Up @@ -2241,6 +2241,7 @@ def __init__(self, a, *, b) -> None:

with warnings.catch_warnings(record=True) as warnings_log:
CustomRLock(1, b=2)

Copy link
Member

Choose a reason for hiding this comment

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

Stray newline change:

Suggested change

self.assertEqual(warnings_log, [])

class EventTests(lock_tests.EventTests):
Expand Down Expand Up @@ -2358,10 +2359,20 @@ def work():
with self.subTest(name=name, expected=expected):
work_name = None
thread = threading.Thread(target=work, name=name)
thread.start()
thread.join()
self.assertEqual(work_name, expected,
f"{len(work_name)=} and {len(expected)=}")
try:
thread.start()
thread.join()
# If the name is non-ASCII and the result is empty, skip (platform limitation)
if any(ord(c) > 127 for c in name) and (not work_name or work_name == ""):
self.skipTest(f"Platform does not support non-ASCII thread names: got empty name for {name!r}")
self.assertEqual(work_name, expected,
f"{len(work_name)=} and {len(expected)=}")
except OSError as exc:
Copy link
Member

Choose a reason for hiding this comment

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

Is OSError even raised here? The test failure was different -- that work_name was unexpectedly empty.

Copy link
Author

@jadonduff jadonduff Aug 22, 2025

Choose a reason for hiding this comment

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

Added:

                    if any(ord(c) > 127 for c in name) and (not work_name or work_name == ""):
                        self.skipTest(f"Platform does not support non-ASCII thread names: got empty name for {name!r}")
                    self.assertEqual(work_name, expected,
                                     f"{len(work_name)=} and {len(expected)=}")

Kept OSError fallback because it was shown in original issue, but can remove if needed.

>>> import _thread
>>> _thread.set_name('€')
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
    _thread.set_name('€')
    ~~~~~~~~~~~~~~~~^^^^^
OSError: [Errno 22] Invalid argument

Copy link
Member

Choose a reason for hiding this comment

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

It was shown that _thread.set_name() fails, but that OSError is not leaked from the Thread code.

# Accept EINVAL (22) for non-ASCII names on platforms that do not support them
if getattr(exc, 'errno', None) == 22 and any(ord(c) > 127 for c in name):
self.skipTest(f"Platform does not support non-ASCII thread names: {exc}")
else:
raise

@unittest.skipUnless(hasattr(_thread, 'set_name'), "missing _thread.set_name")
@unittest.skipUnless(hasattr(_thread, '_get_name'), "missing _thread._get_name")
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,7 @@ Weilin Du
John DuBois
Paul Dubois
Jacques Ducasse
Jadon Duff
Andrei Dorian Duma
Graham Dumpleton
Quinn Dunkan
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_thread.set_name() now retries with an ASCII fallback if pthread_setname_np() rejects UTF-8 names on some POSIX-compliant platforms.
84 changes: 61 additions & 23 deletions Modules/_threadmodule.c
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,22 @@ get_thread_state_by_cls(PyTypeObject *cls)
return get_thread_state(module);
}


#ifdef MS_WINDOWS
typedef HRESULT (WINAPI *PF_GET_THREAD_DESCRIPTION)(HANDLE, PCWSTR*);
typedef HRESULT (WINAPI *PF_SET_THREAD_DESCRIPTION)(HANDLE, PCWSTR);
static PF_GET_THREAD_DESCRIPTION pGetThreadDescription = NULL;
static PF_SET_THREAD_DESCRIPTION pSetThreadDescription = NULL;
#endif

#if defined(HAVE_PTHREAD_SETNAME_NP) || defined(HAVE_PTHREAD_SET_NAME_NP)
static int _set_thread_name(const char *name);
#endif

// Fallback: Provides a no-op implementation if neither pthread naming API is available. This avoids linker errors and provides a portable stub.
#if !defined(HAVE_PTHREAD_SETNAME_NP) && !defined(HAVE_PTHREAD_SET_NAME_NP)
static int _set_thread_name(const char *name) { return 0; }
#endif


/*[clinic input]
module _thread
Expand Down Expand Up @@ -2576,55 +2584,85 @@ _thread.set_name
Set the name of the current thread.
[clinic start generated code]*/


#ifndef MS_WINDOWS
// Helper to set the thread name using platform-specific APIs (POSIX only)
static int
_set_thread_name(const char *name)
{
int rc;
#ifdef __APPLE__
rc = pthread_setname_np(name);
#elif defined(__NetBSD__)
pthread_t thread = pthread_self();
rc = pthread_setname_np(thread, "%s", (void *)name);
#elif defined(HAVE_PTHREAD_SETNAME_NP)
pthread_t thread = pthread_self();
rc = pthread_setname_np(thread, name);
#else /* defined(HAVE_PTHREAD_SET_NAME_NP) */
pthread_t thread = pthread_self();
rc = 0; /* pthread_set_name_np() returns void */
pthread_set_name_np(thread, name);
#endif
return rc;
}
#endif // !MS_WINDOWS


static PyObject *
_thread_set_name_impl(PyObject *module, PyObject *name_obj)
/*[clinic end generated code: output=402b0c68e0c0daed input=7e7acd98261be82f]*/
{
#ifndef MS_WINDOWS
// POSIX and non-Windows platforms
#ifdef __sun
// Solaris always uses UTF-8
const char *encoding = "utf-8";
#else
// Encode the thread name to the filesystem encoding using the "replace"
// error handler
PyInterpreterState *interp = _PyInterpreterState_GET();
const char *encoding = interp->unicode.fs_codec.encoding;
#endif
PyObject *name_encoded;
int rc;

name_encoded = PyUnicode_AsEncodedString(name_obj, encoding, "replace");
if (name_encoded == NULL) {
return NULL;
}

#ifdef _PYTHREAD_NAME_MAXLEN
// Truncate to _PYTHREAD_NAME_MAXLEN bytes + the NUL byte if needed
if (PyBytes_GET_SIZE(name_encoded) > _PYTHREAD_NAME_MAXLEN) {
PyObject *truncated;
truncated = PyBytes_FromStringAndSize(PyBytes_AS_STRING(name_encoded),
_PYTHREAD_NAME_MAXLEN);
PyObject *truncated = PyBytes_FromStringAndSize(PyBytes_AS_STRING(name_encoded), _PYTHREAD_NAME_MAXLEN);
if (truncated == NULL) {
Py_DECREF(name_encoded);
return NULL;
}
Py_SETREF(name_encoded, truncated);
}
#endif

const char *name = PyBytes_AS_STRING(name_encoded);
#ifdef __APPLE__
int rc = pthread_setname_np(name);
#elif defined(__NetBSD__)
pthread_t thread = pthread_self();
int rc = pthread_setname_np(thread, "%s", (void *)name);
#elif defined(HAVE_PTHREAD_SETNAME_NP)
pthread_t thread = pthread_self();
int rc = pthread_setname_np(thread, name);
#else /* defined(HAVE_PTHREAD_SET_NAME_NP) */
pthread_t thread = pthread_self();
int rc = 0; /* pthread_set_name_np() returns void */
pthread_set_name_np(thread, name);
#endif
rc = _set_thread_name(name);
Py_DECREF(name_encoded);

// Fallback: If EINVAL, try ASCII encoding with "replace"
if (rc == EINVAL) {
name_encoded = PyUnicode_AsEncodedString(name_obj, "ascii", "replace");
if (name_encoded == NULL) {
return NULL;
}
#ifdef _PYTHREAD_NAME_MAXLEN
if (PyBytes_GET_SIZE(name_encoded) > _PYTHREAD_NAME_MAXLEN) {
PyObject *truncated = PyBytes_FromStringAndSize(PyBytes_AS_STRING(name_encoded), _PYTHREAD_NAME_MAXLEN);
if (truncated == NULL) {
Py_DECREF(name_encoded);
return NULL;
}
Py_SETREF(name_encoded, truncated);
}
#endif
Copy link
Member

Choose a reason for hiding this comment

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

Again, please try to avoid duplicating code. This should be factored out into its own function. Here's an outline:

static PyObject *
get_truncated(PyObject *name_encoded /* stolen */)
{
#ifdef _PYTHREAD_NAME_MAXLEN
    if (PyBytes_GET_SIZE(name_encoded) > _PYTHREAD_NAME_MAXLEN) {
        PyObject *truncated = PyBytes_FromStringAndSize(PyBytes_AS_STRING(name_encoded), _PYTHREAD_NAME_MAXLEN);
        if (truncated == NULL) {
            Py_DECREF(name_encoded);
            return NULL;
        }
        Py_SETREF(name_encoded, truncated);
    }
#endif
    return name_encoded;
}

Copy link
Member

Choose a reason for hiding this comment

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

Or simply include the encoding and truncating code in _set_thread_name(). BTW, there is no need to use underscored names here.

Copy link
Author

Choose a reason for hiding this comment

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

Created function encode_thread_name and used to replace duplicated code

name = PyBytes_AS_STRING(name_encoded);
rc = _set_thread_name(name);
Py_DECREF(name_encoded);
}

if (rc) {
errno = rc;
return PyErr_SetFromErrno(PyExc_OSError);
Expand Down
Loading