[Checkins] SVN: zc.thread/trunk/ thread (and process) convenience library

Jim Fulton jim at zope.com
Wed Nov 23 23:53:41 UTC 2011


Log message for revision 123478:
  thread (and process) convenience library

Changed:
  U   zc.thread/trunk/README.txt
  U   zc.thread/trunk/buildout.cfg
  U   zc.thread/trunk/setup.py
  A   zc.thread/trunk/src/zc/thread/
  A   zc.thread/trunk/src/zc/thread/__init__.py
  A   zc.thread/trunk/src/zc/thread/tests.py

-=-
Modified: zc.thread/trunk/README.txt
===================================================================
--- zc.thread/trunk/README.txt	2011-11-23 23:52:35 UTC (rev 123477)
+++ zc.thread/trunk/README.txt	2011-11-23 23:53:41 UTC (rev 123478)
@@ -1,10 +1,48 @@
-Title Here
-**********
+Thread-creation helper
+**********************
 
+The thread-creation API provided by the Python ``threading`` module is
+annoying. :)
 
-To learn more, see
+This package provides a very simple thread-creation API that:
 
+- Makes threads daemonic and allows daemonicity to be passed to the
+  constructor.  For example::
 
+    zc.thread.Thread(mythreadfunc)
+
+  Starts a daemonic thread named ``'mythreadfunc'`` running
+  ``mythreadfunc``.
+
+- Allows threads to be defined via decorators, as in::
+
+    import zc.thread
+
+    @zc.thread.Thread
+    def mythread():
+        ...
+
+  In the example above, a daemonic thread named ``mythread`` is
+  created and started.  The thread is also assigned to the variable
+  ``mythread``.
+
+  You can control whether threads are daemonic and wether they are
+  started by default::
+
+    import zc.thread
+
+    @zc.thread.Thread(daemon=False, start=False)
+    def mythread():
+        ...
+
+- After a thread finished, you can get the return value of the
+  target function from the thread's ``value`` attribute, or, if the
+  function raises an exception, you can get the exception object from
+  the thread's ``exception`` attribute.
+
+There's also a Process constructor/decorator that works like Thread,
+but with multi-processing processes.
+
 Changes
 *******
 

Modified: zc.thread/trunk/buildout.cfg
===================================================================
--- zc.thread/trunk/buildout.cfg	2011-11-23 23:52:35 UTC (rev 123477)
+++ zc.thread/trunk/buildout.cfg	2011-11-23 23:53:41 UTC (rev 123478)
@@ -1,12 +1,12 @@
 [buildout]
 develop = .
-parts = test py
+parts = test
 
-[test]
-recipe = zc.recipe.testrunner
-eggs = 
-
 [py]
 recipe = zc.recipe.egg
-eggs = ${test:eggs}
+eggs = zc.thread [test]
 interpreter = py
+
+[test]
+recipe = zc.recipe.testrunner
+eggs = ${py:eggs}

Modified: zc.thread/trunk/setup.py
===================================================================
--- zc.thread/trunk/setup.py	2011-11-23 23:52:35 UTC (rev 123477)
+++ zc.thread/trunk/setup.py	2011-11-23 23:53:41 UTC (rev 123478)
@@ -11,10 +11,10 @@
 # FOR A PARTICULAR PURPOSE.
 #
 ##############################################################################
-name, version = 'zc.', '0'
+name, version = 'zc.thread', '0'
 
 install_requires = ['setuptools']
-extras_require = dict(test=['zope.testing'])
+extras_require = dict(test=['zope.testing', 'mock'])
 
 entry_points = """
 """

Added: zc.thread/trunk/src/zc/thread/__init__.py
===================================================================
--- zc.thread/trunk/src/zc/thread/__init__.py	                        (rev 0)
+++ zc.thread/trunk/src/zc/thread/__init__.py	2011-11-23 23:53:41 UTC (rev 123478)
@@ -0,0 +1,110 @@
+##############################################################################
+#
+# Copyright (c) Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+
+def _options(daemon=True, start=True, args=(), kwargs=None):
+    return daemon, start, args, kwargs or {}
+
+
+def Thread(func=None, **options):
+    """Create and (typically) start a thread
+
+    If no function is passed, then a decorator function is
+    returned. Typical usage is::
+
+       @zc.thread.Thread
+       def mythreadfunc():
+           ...
+
+       ...
+
+       mythread.join()
+
+    Options:
+
+        deamon=True
+           Thread daemon flag. Set to false to cause process exit to
+           block until the thread has exited.
+
+        start=True
+           True to automatically start the thread.
+
+        args=()
+           Positional arguments to pass to the thread function.
+
+        kwargs={}
+           keyword arguments to pass to the thread function.
+
+    """
+    if func is None:
+        return lambda f: Thread(f, **options)
+    daemon, start, args, kwargs = _options(**options)
+    import threading
+
+    def run(*args, **kw):
+        try:
+            v = func(*args, **kw)
+            thread.value = v
+        except Exception, v:
+            thread.exception = v
+
+    thread = threading.Thread(
+        target=run, name=getattr(func, '__name__', None),
+        args=args, kwargs=kwargs)
+    thread.setDaemon(daemon)
+    thread.value = thread.exception = None
+    if start:
+        thread.start()
+    return thread
+
+def Process(func=None, **options):
+    """Create and (typically) start a multiprocessing process
+
+    If no function is passed, then a decorator function is
+    returned. Typical usage is::
+
+       @zc.thread.Process
+       def mythreadfunc():
+           ...
+
+       ...
+
+       mythread.join()
+
+    Options:
+
+        deamon=True
+           Process daemon flag. Set to false to cause process exit to
+           block until the process has exited.
+
+        start=True
+           True to automatically start the process.
+
+        args=()
+           Positional arguments to pass to the process function.
+
+        kwargs={}
+           keyword arguments to pass to the process function.
+
+    """
+    if func is None:
+        return lambda f: Process(f, **options)
+    daemon, start, args, kwargs = _options(**options)
+    import multiprocessing
+    process = multiprocessing.Process(
+        target=func, name=getattr(func, '__name__', None),
+        args=args, kwargs=kwargs)
+    process.daemon = daemon
+    if start:
+        process.start()
+    return process


Property changes on: zc.thread/trunk/src/zc/thread/__init__.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: zc.thread/trunk/src/zc/thread/tests.py
===================================================================
--- zc.thread/trunk/src/zc/thread/tests.py	                        (rev 0)
+++ zc.thread/trunk/src/zc/thread/tests.py	2011-11-23 23:53:41 UTC (rev 123478)
@@ -0,0 +1,116 @@
+##############################################################################
+#
+# Copyright (c) Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+import doctest
+import mock
+import os
+import unittest
+import zc.thread
+
+class TestThread(unittest.TestCase):
+
+    def test_default(self):
+        with mock.patch('threading.Thread') as Thread:
+            @zc.thread.Thread
+            def foo():
+                return 42
+
+            Thread.call_args[1].pop('target')()
+            self.assert_(foo.value == 42 and foo.exception is None)
+            Thread.assert_called_with(name='foo', args=(), kwargs={})
+            foo.setDaemon.assert_called_with(True)
+            foo.start.assert_called_with()
+
+    def test_undecorated_and_exception_return(self):
+        with mock.patch('threading.Thread') as Thread:
+            def foo2():
+                raise ValueError(42)
+
+            t = zc.thread.Thread(foo2)
+            Thread.call_args[1].pop('target')()
+            Thread.assert_called_with(name='foo2', args=(), kwargs=dict())
+            t.setDaemon.assert_called_with(True)
+            t.start.assert_called_with()
+            self.assert_(t.value is None)
+            self.assert_(isinstance(t.exception, ValueError))
+            self.assert_(t.exception.args == (42,))
+
+            t = zc.thread.Thread(foo2, args=(1, 2))
+            Thread.call_args[1].pop('target')(1, 2)
+            Thread.assert_called_with(name='foo2', args=(1, 2), kwargs=dict())
+            t.setDaemon.assert_called_with(True)
+            t.start.assert_called_with()
+            self.assert_(t.value is None)
+            self.assert_(isinstance(t.exception, TypeError))
+
+    def test_passing_arguments(self):
+        with mock.patch('threading.Thread') as Thread:
+            @zc.thread.Thread(args=(1, 2), kwargs=dict(a=1), daemon=False,
+                              start=False)
+            def foo(*a, **k):
+                return a, k
+
+            Thread.call_args[1].pop('target')(1, 2, **dict(a=1))
+            self.assert_(foo.value == ((1, 2), dict(a=1)))
+            Thread.assert_called_with(name='foo', args=(1, 2), kwargs=dict(a=1))
+            foo.setDaemon.assert_called_with(False)
+            self.assert_(not foo.start.called)
+
+    def test_Thread_wo_mock(self):
+        @zc.thread.Thread
+        def foo():
+            return 42
+
+        foo.join()
+        self.assert_(foo.value == 42)
+
+    def test_Process_w_mock(self):
+        with mock.patch('multiprocessing.Process') as Process:
+            @zc.thread.Process
+            def foo():
+                print 'foo called'
+            Process.call_args[1].pop('target')()
+            Process.assert_called_with(name='foo', args=(), kwargs={})
+            self.assert_(foo.daamon)
+            foo.start.assert_called_with()
+            Process.reset_mock()
+
+            def foo2():
+                pass
+            t = zc.thread.Process(foo2)
+            Process.assert_called_with(
+                target=foo2, name='foo2', args=(), kwargs={})
+            self.assert_(t.daamon)
+            t.start.assert_called_with()
+            Process.reset_mock()
+
+            @zc.thread.Process(daemon=False, start=False, args=(42,),
+                               kwargs=dict(a=1))
+            def foo3():
+                print 'foo3 called'
+            Process.call_args[1].pop('target')()
+            Process.assert_called_with(name='foo3', args=(42,), kwargs=dict(a=1))
+            self.assert_(not foo3.daemon)
+            self.assert_(not foo3.start.called)
+
+    def test_Process_wo_mock(self):
+        import multiprocessing
+        queue = multiprocessing.Queue()
+        zc.thread.Process(run_process, args=(queue,)).join(11)
+        self.assert_(queue.get() != os.getpid())
+
+def run_process(queue):
+    queue.put(os.getpid())
+
+def test_suite():
+    return unittest.makeSuite(TestThread)


Property changes on: zc.thread/trunk/src/zc/thread/tests.py
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native



More information about the checkins mailing list