From b43c4caf81b10e5c7ebaeb3a712c6db584f60bbd Mon Sep 17 00:00:00 2001 From: Antoine Pitrou Date: Mon, 18 Sep 2017 22:04:20 +0200 Subject: [PATCH] Restore dummy_threading and _dummy_thread, but deprecate them (bpo-31370) (#3648) --- Doc/library/_dummy_thread.rst | 17 +-- Doc/library/_thread.rst | 10 +- Doc/library/concurrency.rst | 2 +- Doc/library/dummy_threading.rst | 17 +-- Doc/library/threading.rst | 4 +- Lib/_dummy_thread.py | 163 ++++++++++++++++++++ Lib/dummy_threading.py | 78 ++++++++++ Lib/test/test_dummy_thread.py | 255 +++++++++++++++++++++++++++++++ Lib/test/test_dummy_threading.py | 60 ++++++++ 9 files changed, 575 insertions(+), 31 deletions(-) create mode 100644 Lib/_dummy_thread.py create mode 100644 Lib/dummy_threading.py create mode 100644 Lib/test/test_dummy_thread.py create mode 100644 Lib/test/test_dummy_threading.py diff --git a/Doc/library/_dummy_thread.rst b/Doc/library/_dummy_thread.rst index ebce74d5a22..7dccbc55475 100644 --- a/Doc/library/_dummy_thread.rst +++ b/Doc/library/_dummy_thread.rst @@ -6,18 +6,15 @@ **Source code:** :source:`Lib/_dummy_thread.py` +.. deprecated:: 3.7 + Python now always has threading enabled. Please use :mod:`_thread` + (or, better, :mod:`threading`) instead. + -------------- -This module provides a duplicate interface to the :mod:`_thread` module. It is -meant to be imported when the :mod:`_thread` module is not provided on a -platform. - -Suggested usage is:: - - try: - import _thread - except ImportError: - import _dummy_thread as _thread +This module provides a duplicate interface to the :mod:`_thread` module. +It was meant to be imported when the :mod:`_thread` module was not provided +on a platform. Be careful to not use this module where deadlock might occur from a thread being created that blocks waiting for another thread to be created. This often occurs diff --git a/Doc/library/_thread.rst b/Doc/library/_thread.rst index 0d2d818f5f9..67cb70944f4 100644 --- a/Doc/library/_thread.rst +++ b/Doc/library/_thread.rst @@ -23,14 +23,10 @@ threading API built on top of this module. single: pthreads pair: threads; POSIX -The module is optional. It is supported on Windows, Linux, SGI IRIX, Solaris -2.x, as well as on systems that have a POSIX thread (a.k.a. "pthread") -implementation. For systems lacking the :mod:`_thread` module, the -:mod:`_dummy_thread` module is available. It duplicates this module's interface -and can be used as a drop-in replacement. - -It defines the following constants and functions: +.. versionchanged:: 3.7 + This module used to be optional, it is now always available. +This module defines the following constants and functions: .. exception:: error diff --git a/Doc/library/concurrency.rst b/Doc/library/concurrency.rst index 0de281bd149..826bf86d081 100644 --- a/Doc/library/concurrency.rst +++ b/Doc/library/concurrency.rst @@ -26,6 +26,6 @@ The following are support modules for some of the above services: .. toctree:: - dummy_threading.rst _thread.rst _dummy_thread.rst + dummy_threading.rst diff --git a/Doc/library/dummy_threading.rst b/Doc/library/dummy_threading.rst index 30a3ebba674..dfc3289abb1 100644 --- a/Doc/library/dummy_threading.rst +++ b/Doc/library/dummy_threading.rst @@ -6,20 +6,15 @@ **Source code:** :source:`Lib/dummy_threading.py` +.. deprecated:: 3.7 + Python now always has threading enabled. Please use :mod:`threading` instead. + -------------- -This module provides a duplicate interface to the :mod:`threading` module. It -is meant to be imported when the :mod:`_thread` module is not provided on a -platform. - -Suggested usage is:: - - try: - import threading - except ImportError: - import dummy_threading as threading +This module provides a duplicate interface to the :mod:`threading` module. +It was meant to be imported when the :mod:`_thread` module was not provided +on a platform. Be careful to not use this module where deadlock might occur from a thread being created that blocks waiting for another thread to be created. This often occurs with blocking I/O. - diff --git a/Doc/library/threading.rst b/Doc/library/threading.rst index 021e29e7124..8c3a2cd4f65 100644 --- a/Doc/library/threading.rst +++ b/Doc/library/threading.rst @@ -11,8 +11,8 @@ This module constructs higher-level threading interfaces on top of the lower level :mod:`_thread` module. See also the :mod:`queue` module. -The :mod:`dummy_threading` module is provided for situations where -:mod:`threading` cannot be used because :mod:`_thread` is missing. +.. versionchanged:: 3.7 + This module used to be optional, it is now always available. .. note:: diff --git a/Lib/_dummy_thread.py b/Lib/_dummy_thread.py new file mode 100644 index 00000000000..a2cae54b058 --- /dev/null +++ b/Lib/_dummy_thread.py @@ -0,0 +1,163 @@ +"""Drop-in replacement for the thread module. + +Meant to be used as a brain-dead substitute so that threaded code does +not need to be rewritten for when the thread module is not present. + +Suggested usage is:: + + try: + import _thread + except ImportError: + import _dummy_thread as _thread + +""" +# Exports only things specified by thread documentation; +# skipping obsolete synonyms allocate(), start_new(), exit_thread(). +__all__ = ['error', 'start_new_thread', 'exit', 'get_ident', 'allocate_lock', + 'interrupt_main', 'LockType'] + +# A dummy value +TIMEOUT_MAX = 2**31 + +# NOTE: this module can be imported early in the extension building process, +# and so top level imports of other modules should be avoided. Instead, all +# imports are done when needed on a function-by-function basis. Since threads +# are disabled, the import lock should not be an issue anyway (??). + +error = RuntimeError + +def start_new_thread(function, args, kwargs={}): + """Dummy implementation of _thread.start_new_thread(). + + Compatibility is maintained by making sure that ``args`` is a + tuple and ``kwargs`` is a dictionary. If an exception is raised + and it is SystemExit (which can be done by _thread.exit()) it is + caught and nothing is done; all other exceptions are printed out + by using traceback.print_exc(). + + If the executed function calls interrupt_main the KeyboardInterrupt will be + raised when the function returns. + + """ + if type(args) != type(tuple()): + raise TypeError("2nd arg must be a tuple") + if type(kwargs) != type(dict()): + raise TypeError("3rd arg must be a dict") + global _main + _main = False + try: + function(*args, **kwargs) + except SystemExit: + pass + except: + import traceback + traceback.print_exc() + _main = True + global _interrupt + if _interrupt: + _interrupt = False + raise KeyboardInterrupt + +def exit(): + """Dummy implementation of _thread.exit().""" + raise SystemExit + +def get_ident(): + """Dummy implementation of _thread.get_ident(). + + Since this module should only be used when _threadmodule is not + available, it is safe to assume that the current process is the + only thread. Thus a constant can be safely returned. + """ + return 1 + +def allocate_lock(): + """Dummy implementation of _thread.allocate_lock().""" + return LockType() + +def stack_size(size=None): + """Dummy implementation of _thread.stack_size().""" + if size is not None: + raise error("setting thread stack size not supported") + return 0 + +def _set_sentinel(): + """Dummy implementation of _thread._set_sentinel().""" + return LockType() + +class LockType(object): + """Class implementing dummy implementation of _thread.LockType. + + Compatibility is maintained by maintaining self.locked_status + which is a boolean that stores the state of the lock. Pickling of + the lock, though, should not be done since if the _thread module is + then used with an unpickled ``lock()`` from here problems could + occur from this class not having atomic methods. + + """ + + def __init__(self): + self.locked_status = False + + def acquire(self, waitflag=None, timeout=-1): + """Dummy implementation of acquire(). + + For blocking calls, self.locked_status is automatically set to + True and returned appropriately based on value of + ``waitflag``. If it is non-blocking, then the value is + actually checked and not set if it is already acquired. This + is all done so that threading.Condition's assert statements + aren't triggered and throw a little fit. + + """ + if waitflag is None or waitflag: + self.locked_status = True + return True + else: + if not self.locked_status: + self.locked_status = True + return True + else: + if timeout > 0: + import time + time.sleep(timeout) + return False + + __enter__ = acquire + + def __exit__(self, typ, val, tb): + self.release() + + def release(self): + """Release the dummy lock.""" + # XXX Perhaps shouldn't actually bother to test? Could lead + # to problems for complex, threaded code. + if not self.locked_status: + raise error + self.locked_status = False + return True + + def locked(self): + return self.locked_status + + def __repr__(self): + return "<%s %s.%s object at %s>" % ( + "locked" if self.locked_status else "unlocked", + self.__class__.__module__, + self.__class__.__qualname__, + hex(id(self)) + ) + +# Used to signal that interrupt_main was called in a "thread" +_interrupt = False +# True when not executing in a "thread" +_main = True + +def interrupt_main(): + """Set _interrupt flag to True to have start_new_thread raise + KeyboardInterrupt upon exiting.""" + if _main: + raise KeyboardInterrupt + else: + global _interrupt + _interrupt = True diff --git a/Lib/dummy_threading.py b/Lib/dummy_threading.py new file mode 100644 index 00000000000..1bb7eee338a --- /dev/null +++ b/Lib/dummy_threading.py @@ -0,0 +1,78 @@ +"""Faux ``threading`` version using ``dummy_thread`` instead of ``thread``. + +The module ``_dummy_threading`` is added to ``sys.modules`` in order +to not have ``threading`` considered imported. Had ``threading`` been +directly imported it would have made all subsequent imports succeed +regardless of whether ``_thread`` was available which is not desired. + +""" +from sys import modules as sys_modules + +import _dummy_thread + +# Declaring now so as to not have to nest ``try``s to get proper clean-up. +holding_thread = False +holding_threading = False +holding__threading_local = False + +try: + # Could have checked if ``_thread`` was not in sys.modules and gone + # a different route, but decided to mirror technique used with + # ``threading`` below. + if '_thread' in sys_modules: + held_thread = sys_modules['_thread'] + holding_thread = True + # Must have some module named ``_thread`` that implements its API + # in order to initially import ``threading``. + sys_modules['_thread'] = sys_modules['_dummy_thread'] + + if 'threading' in sys_modules: + # If ``threading`` is already imported, might as well prevent + # trying to import it more than needed by saving it if it is + # already imported before deleting it. + held_threading = sys_modules['threading'] + holding_threading = True + del sys_modules['threading'] + + if '_threading_local' in sys_modules: + # If ``_threading_local`` is already imported, might as well prevent + # trying to import it more than needed by saving it if it is + # already imported before deleting it. + held__threading_local = sys_modules['_threading_local'] + holding__threading_local = True + del sys_modules['_threading_local'] + + import threading + # Need a copy of the code kept somewhere... + sys_modules['_dummy_threading'] = sys_modules['threading'] + del sys_modules['threading'] + sys_modules['_dummy__threading_local'] = sys_modules['_threading_local'] + del sys_modules['_threading_local'] + from _dummy_threading import * + from _dummy_threading import __all__ + +finally: + # Put back ``threading`` if we overwrote earlier + + if holding_threading: + sys_modules['threading'] = held_threading + del held_threading + del holding_threading + + # Put back ``_threading_local`` if we overwrote earlier + + if holding__threading_local: + sys_modules['_threading_local'] = held__threading_local + del held__threading_local + del holding__threading_local + + # Put back ``thread`` if we overwrote, else del the entry we made + if holding_thread: + sys_modules['_thread'] = held_thread + del held_thread + else: + del sys_modules['_thread'] + del holding_thread + + del _dummy_thread + del sys_modules diff --git a/Lib/test/test_dummy_thread.py b/Lib/test/test_dummy_thread.py new file mode 100644 index 00000000000..0840be67d56 --- /dev/null +++ b/Lib/test/test_dummy_thread.py @@ -0,0 +1,255 @@ +import _dummy_thread as _thread +import time +import queue +import random +import unittest +from test import support +from unittest import mock + +DELAY = 0 + + +class LockTests(unittest.TestCase): + """Test lock objects.""" + + def setUp(self): + # Create a lock + self.lock = _thread.allocate_lock() + + def test_initlock(self): + #Make sure locks start locked + self.assertFalse(self.lock.locked(), + "Lock object is not initialized unlocked.") + + def test_release(self): + # Test self.lock.release() + self.lock.acquire() + self.lock.release() + self.assertFalse(self.lock.locked(), + "Lock object did not release properly.") + + def test_LockType_context_manager(self): + with _thread.LockType(): + pass + self.assertFalse(self.lock.locked(), + "Acquired Lock was not released") + + def test_improper_release(self): + #Make sure release of an unlocked thread raises RuntimeError + self.assertRaises(RuntimeError, self.lock.release) + + def test_cond_acquire_success(self): + #Make sure the conditional acquiring of the lock works. + self.assertTrue(self.lock.acquire(0), + "Conditional acquiring of the lock failed.") + + def test_cond_acquire_fail(self): + #Test acquiring locked lock returns False + self.lock.acquire(0) + self.assertFalse(self.lock.acquire(0), + "Conditional acquiring of a locked lock incorrectly " + "succeeded.") + + def test_uncond_acquire_success(self): + #Make sure unconditional acquiring of a lock works. + self.lock.acquire() + self.assertTrue(self.lock.locked(), + "Uncondional locking failed.") + + def test_uncond_acquire_return_val(self): + #Make sure that an unconditional locking returns True. + self.assertIs(self.lock.acquire(1), True, + "Unconditional locking did not return True.") + self.assertIs(self.lock.acquire(), True) + + def test_uncond_acquire_blocking(self): + #Make sure that unconditional acquiring of a locked lock blocks. + def delay_unlock(to_unlock, delay): + """Hold on to lock for a set amount of time before unlocking.""" + time.sleep(delay) + to_unlock.release() + + self.lock.acquire() + start_time = int(time.time()) + _thread.start_new_thread(delay_unlock,(self.lock, DELAY)) + if support.verbose: + print() + print("*** Waiting for thread to release the lock "\ + "(approx. %s sec.) ***" % DELAY) + self.lock.acquire() + end_time = int(time.time()) + if support.verbose: + print("done") + self.assertGreaterEqual(end_time - start_time, DELAY, + "Blocking by unconditional acquiring failed.") + + @mock.patch('time.sleep') + def test_acquire_timeout(self, mock_sleep): + """Test invoking acquire() with a positive timeout when the lock is + already acquired. Ensure that time.sleep() is invoked with the given + timeout and that False is returned.""" + + self.lock.acquire() + retval = self.lock.acquire(waitflag=0, timeout=1) + self.assertTrue(mock_sleep.called) + mock_sleep.assert_called_once_with(1) + self.assertEqual(retval, False) + + def test_lock_representation(self): + self.lock.acquire() + self.assertIn("locked", repr(self.lock)) + self.lock.release() + self.assertIn("unlocked", repr(self.lock)) + + +class MiscTests(unittest.TestCase): + """Miscellaneous tests.""" + + def test_exit(self): + self.assertRaises(SystemExit, _thread.exit) + + def test_ident(self): + self.assertIsInstance(_thread.get_ident(), int, + "_thread.get_ident() returned a non-integer") + self.assertGreater(_thread.get_ident(), 0) + + def test_LockType(self): + self.assertIsInstance(_thread.allocate_lock(), _thread.LockType, + "_thread.LockType is not an instance of what " + "is returned by _thread.allocate_lock()") + + def test_set_sentinel(self): + self.assertIsInstance(_thread._set_sentinel(), _thread.LockType, + "_thread._set_sentinel() did not return a " + "LockType instance.") + + def test_interrupt_main(self): + #Calling start_new_thread with a function that executes interrupt_main + # should raise KeyboardInterrupt upon completion. + def call_interrupt(): + _thread.interrupt_main() + + self.assertRaises(KeyboardInterrupt, + _thread.start_new_thread, + call_interrupt, + tuple()) + + def test_interrupt_in_main(self): + self.assertRaises(KeyboardInterrupt, _thread.interrupt_main) + + def test_stack_size_None(self): + retval = _thread.stack_size(None) + self.assertEqual(retval, 0) + + def test_stack_size_not_None(self): + with self.assertRaises(_thread.error) as cm: + _thread.stack_size("") + self.assertEqual(cm.exception.args[0], + "setting thread stack size not supported") + + +class ThreadTests(unittest.TestCase): + """Test thread creation.""" + + def test_arg_passing(self): + #Make sure that parameter passing works. + def arg_tester(queue, arg1=False, arg2=False): + """Use to test _thread.start_new_thread() passes args properly.""" + queue.put((arg1, arg2)) + + testing_queue = queue.Queue(1) + _thread.start_new_thread(arg_tester, (testing_queue, True, True)) + result = testing_queue.get() + self.assertTrue(result[0] and result[1], + "Argument passing for thread creation " + "using tuple failed") + + _thread.start_new_thread( + arg_tester, + tuple(), + {'queue':testing_queue, 'arg1':True, 'arg2':True}) + + result = testing_queue.get() + self.assertTrue(result[0] and result[1], + "Argument passing for thread creation " + "using kwargs failed") + + _thread.start_new_thread( + arg_tester, + (testing_queue, True), + {'arg2':True}) + + result = testing_queue.get() + self.assertTrue(result[0] and result[1], + "Argument passing for thread creation using both tuple" + " and kwargs failed") + + def test_multi_thread_creation(self): + def queue_mark(queue, delay): + time.sleep(delay) + queue.put(_thread.get_ident()) + + thread_count = 5 + testing_queue = queue.Queue(thread_count) + + if support.verbose: + print() + print("*** Testing multiple thread creation " + "(will take approx. %s to %s sec.) ***" % ( + DELAY, thread_count)) + + for count in range(thread_count): + if DELAY: + local_delay = round(random.random(), 1) + else: + local_delay = 0 + _thread.start_new_thread(queue_mark, + (testing_queue, local_delay)) + time.sleep(DELAY) + if support.verbose: + print('done') + self.assertEqual(testing_queue.qsize(), thread_count, + "Not all %s threads executed properly " + "after %s sec." % (thread_count, DELAY)) + + def test_args_not_tuple(self): + """ + Test invoking start_new_thread() with a non-tuple value for "args". + Expect TypeError with a meaningful error message to be raised. + """ + with self.assertRaises(TypeError) as cm: + _thread.start_new_thread(mock.Mock(), []) + self.assertEqual(cm.exception.args[0], "2nd arg must be a tuple") + + def test_kwargs_not_dict(self): + """ + Test invoking start_new_thread() with a non-dict value for "kwargs". + Expect TypeError with a meaningful error message to be raised. + """ + with self.assertRaises(TypeError) as cm: + _thread.start_new_thread(mock.Mock(), tuple(), kwargs=[]) + self.assertEqual(cm.exception.args[0], "3rd arg must be a dict") + + def test_SystemExit(self): + """ + Test invoking start_new_thread() with a function that raises + SystemExit. + The exception should be discarded. + """ + func = mock.Mock(side_effect=SystemExit()) + try: + _thread.start_new_thread(func, tuple()) + except SystemExit: + self.fail("start_new_thread raised SystemExit.") + + @mock.patch('traceback.print_exc') + def test_RaiseException(self, mock_print_exc): + """ + Test invoking start_new_thread() with a function that raises exception. + + The exception should be discarded and the traceback should be printed + via traceback.print_exc() + """ + func = mock.Mock(side_effect=Exception) + _thread.start_new_thread(func, tuple()) + self.assertTrue(mock_print_exc.called) diff --git a/Lib/test/test_dummy_threading.py b/Lib/test/test_dummy_threading.py new file mode 100644 index 00000000000..a0c2972a60e --- /dev/null +++ b/Lib/test/test_dummy_threading.py @@ -0,0 +1,60 @@ +from test import support +import unittest +import dummy_threading as _threading +import time + +class DummyThreadingTestCase(unittest.TestCase): + + class TestThread(_threading.Thread): + + def run(self): + global running + global sema + global mutex + # Uncomment if testing another module, such as the real 'threading' + # module. + #delay = random.random() * 2 + delay = 0 + if support.verbose: + print('task', self.name, 'will run for', delay, 'sec') + sema.acquire() + mutex.acquire() + running += 1 + if support.verbose: + print(running, 'tasks are running') + mutex.release() + time.sleep(delay) + if support.verbose: + print('task', self.name, 'done') + mutex.acquire() + running -= 1 + if support.verbose: + print(self.name, 'is finished.', running, 'tasks are running') + mutex.release() + sema.release() + + def setUp(self): + self.numtasks = 10 + global sema + sema = _threading.BoundedSemaphore(value=3) + global mutex + mutex = _threading.RLock() + global running + running = 0 + self.threads = [] + + def test_tasks(self): + for i in range(self.numtasks): + t = self.TestThread(name=""%i) + self.threads.append(t) + t.start() + + if support.verbose: + print('waiting for all tasks to complete') + for t in self.threads: + t.join() + if support.verbose: + print('all tasks done') + +if __name__ == '__main__': + unittest.main()