diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index 7fcf93d7461..7eb12fe116b 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -264,8 +264,10 @@ since it is impossible to detect the termination of alien threads. *target* is the callable object to be invoked by the :meth:`run` method. Defaults to ``None``, meaning nothing is called. - *name* is the thread name. By default, a unique name is constructed of the - form "Thread-*N*" where *N* is a small decimal number. + *name* is the thread name. By default, a unique name is constructed + of the form "Thread-*N*" where *N* is a small decimal number, + or "Thread-*N* (target)" where "target" is ``target.__name__`` if the + *target* argument is specified. *args* is the argument tuple for the target invocation. Defaults to ``()``. @@ -280,6 +282,9 @@ since it is impossible to detect the termination of alien threads. base class constructor (``Thread.__init__()``) before doing anything else to the thread. + .. versionchanged:: 3.10 + Use the *target* name if *name* argument is omitted. + .. versionchanged:: 3.3 Added the *daemon* argument. diff --git a/Lib/test/test_threading.py b/Lib/test/test_threading.py index d02d3b346f9..2f0f3ae0946 100644 --- a/Lib/test/test_threading.py +++ b/Lib/test/test_threading.py @@ -20,6 +20,7 @@ import subprocess import signal import textwrap +from unittest import mock from test import lock_tests from test import support @@ -86,6 +87,33 @@ class BaseTestCase(unittest.TestCase): class ThreadTests(BaseTestCase): + @cpython_only + def test_name(self): + def func(): pass + + thread = threading.Thread(name="myname1") + self.assertEqual(thread.name, "myname1") + + # Convert int name to str + thread = threading.Thread(name=123) + self.assertEqual(thread.name, "123") + + # target name is ignored if name is specified + thread = threading.Thread(target=func, name="myname2") + self.assertEqual(thread.name, "myname2") + + with mock.patch.object(threading, '_counter', return_value=2): + thread = threading.Thread(name="") + self.assertEqual(thread.name, "Thread-2") + + with mock.patch.object(threading, '_counter', return_value=3): + thread = threading.Thread() + self.assertEqual(thread.name, "Thread-3") + + with mock.patch.object(threading, '_counter', return_value=5): + thread = threading.Thread(target=func) + self.assertEqual(thread.name, "Thread-5 (func)") + # Create a bunch of threads, let each do some work, wait until all are # done. def test_various_ops(self): @@ -531,7 +559,7 @@ class ThreadTests(BaseTestCase): import os, threading, sys from test import support - def f(): + def func(): pid = os.fork() if pid == 0: main = threading.main_thread() @@ -544,14 +572,14 @@ class ThreadTests(BaseTestCase): else: support.wait_process(pid, exitcode=0) - th = threading.Thread(target=f) + th = threading.Thread(target=func) th.start() th.join() """ _, out, err = assert_python_ok("-c", code) data = out.decode().replace('\r', '') self.assertEqual(err, b"") - self.assertEqual(data, "Thread-1\nTrue\nTrue\n") + self.assertEqual(data, "Thread-1 (func)\nTrue\nTrue\n") def test_main_thread_during_shutdown(self): # bpo-31516: current_thread() should still point to the main thread diff --git a/Lib/threading.py b/Lib/threading.py index ab29db77a74..06c77f70fe7 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -745,10 +745,9 @@ class BrokenBarrierError(RuntimeError): # Helper to generate new thread names -_counter = _count().__next__ -_counter() # Consume 0 so first non-main thread has id 1. -def _newname(template="Thread-%d"): - return template % _counter() +_counter = _count(1).__next__ +def _newname(name_template): + return name_template % _counter() # Active thread administration _active_limbo_lock = _allocate_lock() @@ -800,8 +799,19 @@ class Thread: assert group is None, "group argument must be None for now" if kwargs is None: kwargs = {} + if name: + name = str(name) + else: + name = _newname("Thread-%d") + if target is not None: + try: + target_name = target.__name__ + name += f" ({target_name})" + except AttributeError: + pass + self._target = target - self._name = str(name or _newname()) + self._name = name self._args = args self._kwargs = kwargs if daemon is not None: diff --git a/Misc/NEWS.d/next/Library/2020-09-22-13-51-14.bpo-41833.6HVDjT.rst b/Misc/NEWS.d/next/Library/2020-09-22-13-51-14.bpo-41833.6HVDjT.rst new file mode 100644 index 00000000000..abb3a077d91 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-09-22-13-51-14.bpo-41833.6HVDjT.rst @@ -0,0 +1,2 @@ +The :class:`threading.Thread` constructor now uses the target name if the +*target* argument is specified but the *name* argument is omitted.