Repository: ManiacalLabs/BiblioPixel
Pull request #292 (2017-07-17 20:15:43)

Call a subprocess or thread.
We aren't using this in any code yet but I wrote it in some experiments and want to preserve it.


Patch:
diff --git a/bibliopixel/layout/strip.py b/bibliopixel/layout/strip.py
index 543affa..c6a3968 100644
--- a/bibliopixel/layout/strip.py
+++ b/bibliopixel/layout/strip.py
@@ -6,7 +6,7 @@ from . geometry.strip import gen_strip
 class Strip(Layout):
 
     def __init__(self, drivers, threadedUpdate=False,
-                 brightness=255, pixelWidth=1, **kwargs):
+                 brightness=255, pixelWidth=1, coordMap=None, **kwargs):
         super().__init__(drivers, threadedUpdate, brightness,
                          maker=kwargs.get('maker', data_maker.MAKER))
 
@@ -23,17 +23,29 @@ class Strip(Layout):
             self.set = self._setScaled
             self.numLEDs = self.numLEDs / self.pixelWidth
 
+        self.coordMap = coordMap
+        if self.coordMap:
+            if len(self.coordMap) != self.numLEDs:
+                raise ValueError('coordMap length must equal total number of pixels!')
+            self.set_base = self._set_strip_mapped
+        else:
+            self.set_base = self._set_base
+
         self.set_pixel_positions(gen_strip(self.numLEDs))
 
+    def _set_strip_mapped(self, pixel, color):
+        pixel = self.coordMap[pixel]
+        self._set_base(pixel, color)
+
     # Set single pixel to Color value
     def _set(self, pixel, color):
         """Set pixel to RGB color tuple"""
-        self._set_base(pixel, color)
+        self.set_base(pixel, color)
 
     def _setScaled(self, pixel, color):
         start = pixel * self.pixelWidth
         for p in range(start, start + self.pixelWidth):
-            self._set_base(p, color)
+            self.set_base(p, color)
 
     def get(self, pixel):
         """Get RGB color tuple of color at index pixel"""
diff --git a/bibliopixel/project/project.py b/bibliopixel/project/project.py
index ad75b6f..b2e86c0 100644
--- a/bibliopixel/project/project.py
+++ b/bibliopixel/project/project.py
@@ -7,6 +7,7 @@ from .. layout.geometry import gen_matrix
 from .. layout.multimap import MultiMapBuilder
 from .. util import files
 from .. remote import control
+import copy
 
 
 def _make_object(*args, field_types=FIELD_TYPES, **kwds):
@@ -17,10 +18,12 @@ def _make_layout(layout, driver=None, drivers=None, maker=None):
     if driver is None and drivers is None:
         raise ValueError('Projects has no driver or drivers section')
 
+    _layout = copy.deepcopy(layout)
+    coord_map = _layout.get('coordMap', None)
+    _layout.pop('coordMap', None)
+
     if drivers is None:
         drivers = [_make_object(**driver)]
-        coord_map = None
-
     else:
         build = MultiMapBuilder()
 
@@ -33,10 +36,10 @@ def _make_layout(layout, driver=None, drivers=None, maker=None):
             drivers = [dict(driver, **d) for d in drivers]
 
         drivers = [make_driver(**d) for d in drivers]
-        coord_map = build.map
+        coord_map = coord_map or build.map
 
     maker = data_maker.Maker(**(maker or {}))
-    return _make_object(drivers, coordMap=coord_map, maker=maker, **layout)
+    return _make_object(drivers, coordMap=coord_map, maker=maker, **_layout)
 
 
 def make_animation(layout, animation, run=None):
diff --git a/bibliopixel/threads/sub.py b/bibliopixel/threads/sub.py
new file mode 100644
index 0000000..8210971
--- /dev/null
+++ b/bibliopixel/threads/sub.py
@@ -0,0 +1,40 @@
+"""
+More or less uniformly run something as a new daemon thread or process.
+"""
+
+import multiprocessing, threading, queue
+
+
+def _run_locally(input, output, function, args, **kwds):
+    function(input, output, *args, **kwds)
+
+
+def run(function, *args, use_subprocess=False, daemon=True, **kwds):
+    """
+    Create input, output queues, call `function` in a subprocess or a thread.
+
+    `function` is called like this:
+        function(input, output, *args, **kwds)
+
+    Args:
+        use_subprocess: if true, create a new multiprocess;  if false, create
+            a new thread
+        function: the function to call
+        *args, **kwds: arguments to the function.
+        daemon: is the thread or subprocess run as a daemon or not?
+
+    Returns:
+        a tuple with three elements: the subprocess or thread, an input
+        queue, and an output queue.
+    """
+    if use_subprocess:
+        Creator, Queue = multiprocessing.Process, multiprocessing.Queue
+    else:
+        Creator, Queue = threading.Thread, queue.Queue
+
+    input, output = Queue(), Queue()
+    args = input, output, function, args
+    sub = Creator(target=_run_locally, args=args, kwargs=kwds, daemon=daemon)
+    sub.start()
+
+    return sub, input, output


Test patch:
diff --git a/test/sub_test.py b/test/sub_test.py
new file mode 100644
index 0000000..5a1c6d3
--- /dev/null
+++ b/test/sub_test.py
@@ -0,0 +1,34 @@
+import functools, time, unittest
+
+from bibliopixel.threads import sub
+
+pause = functools.partial(time.sleep, 0.005)
+
+
+def run(input, output, *arg, **kwds):
+    pause()
+
+    if arg != (1, 2, 3):
+        raise ValueError('1 2 3')
+
+    if kwds != dict(a=1):
+        raise ValueError('a=1')
+
+    pause()
+
+
+class SubTest(unittest.TestCase):
+    def do_test(self, use_subprocess):
+        s, i, o = sub.run(run, 1, 2, 3, a=1, use_subprocess=use_subprocess)
+        self.assertTrue(s.is_alive())
+        pause()
+        self.assertTrue(s.is_alive())
+        pause()
+        pause()
+        self.assertFalse(s.is_alive())
+
+    def test_subprocess(self):
+        self.do_test(True)
+
+    def test_threading(self):
+        self.do_test(False)