Repository: ManiacalLabs/BiblioPixel
Pull request #886 (2018-07-18 12:57:53)

New colors.to_color, and unittests
Spring cleaning!

Extracted this from an abandoned branch.

`to_color` gets used presently... but also, all those tests...

rec: OK, I just added a more controversial change to this!

I need to fix `Fill`, which is becoming pretty key, and to do so I need features that are only in the dev branch of BiblioPixel.

I'm not going to move every random Animation in, but this one is so key to almost every animation with over a few dozen components...

Patch:
diff --git a/bibliopixel/animation/fill.py b/bibliopixel/animation/fill.py
new file mode 100644
index 0000000..c3435ae
--- /dev/null
+++ b/bibliopixel/animation/fill.py
@@ -0,0 +1,31 @@
+from . animation import BaseAnimation
+from .. util import colors
+
+
+class Fill(BaseAnimation):
+    """
+    Fill the screen with a single color.
+    """
+    def __init__(self, *args, color='black', **kwds):
+        super().__init__(*args, preclear=False, **kwds)
+
+        is_numpy = hasattr(self.color_list, 'dtype')
+        self._set_color = self._set_numpy if is_numpy else self._set_classic
+
+    def pre_run(self):
+        self.color = self._color
+
+    @property
+    def color(self):
+        return self._color
+
+    @color.setter
+    def color(self, color):
+        self._color = colors.make_color(color)
+        self._set_color()
+
+    def _set_numpy(self):
+        self.color_list[:None] = self._color
+
+    def _set_classic(self):
+        self.color_list[:] = [self._color] * len(self.color_list)
diff --git a/bibliopixel/animation/sequence.py b/bibliopixel/animation/sequence.py
index 786b8a9..88e4d85 100644
--- a/bibliopixel/animation/sequence.py
+++ b/bibliopixel/animation/sequence.py
@@ -25,8 +25,8 @@ class Sequence(wrapper.Indexed):
         self.index = 0
 
     def pre_run(self):
-        super().pre_run()
         self.offset = 0
+        super().pre_run()
         self.restart()
 
     def step(self, amt=1):
diff --git a/bibliopixel/control/rest.py b/bibliopixel/control/rest.py
index 50af6d3..3d49551 100644
--- a/bibliopixel/control/rest.py
+++ b/bibliopixel/control/rest.py
@@ -1,5 +1,6 @@
 import contextlib, flask, fractions, functools, traceback
 import http.client, werkzeug.serving
+from werkzeug.datastructures import ImmutableOrderedMultiDict
 
 from . import control, editor
 from .. util import log
@@ -93,12 +94,19 @@ form data `levels=[1, 1, 1]&master=0.5` does the same as the previous example.
 """
 
 
+class OrderedFlask(flask.Flask):
+    # http://flask.pocoo.org/docs/1.0/patterns/subclassing/
+
+    class request_class(flask.Request):
+        parameter_storage_class = ImmutableOrderedMultiDict
+
+
 class RestServer(runnable.LoopThread):
     def __init__(self, port, external_access):
         super().__init__()
         self.port = port
         self.hostname = '0.0.0.0' if external_access else 'localhost'
-        self.app = flask.Flask(__name__)
+        self.app = OrderedFlask(__name__)
 
     def run_once(self):
         werkzeug.serving.run_simple(self.hostname, self.port, self.app)
diff --git a/bibliopixel/util/colors/__init__.py b/bibliopixel/util/colors/__init__.py
index 05f05e7..b1cb033 100644
--- a/bibliopixel/util/colors/__init__.py
+++ b/bibliopixel/util/colors/__init__.py
@@ -50,7 +50,8 @@ from . conversions import hsv2rgb_spectrum, hsv2rgb_rainbow, hsv2rgb_360
 from . conversions import hsv2rgb, hue2rgb, hue_gradient, hue2rgb_360
 from . conversions import hue_helper, hue_helper360
 from . wheel import wheel_color, wheel_helper
-from . names import COLORS, COLOR_DICT, toggle
+from . names import (
+    COLORS, COLOR_DICT, color_to_name, name_to_color, to_color, toggle)
 
 from .. import deprecated
 
diff --git a/bibliopixel/util/colors/names.py b/bibliopixel/util/colors/names.py
index 7fb5967..93931b1 100644
--- a/bibliopixel/util/colors/names.py
+++ b/bibliopixel/util/colors/names.py
@@ -2,7 +2,7 @@
 Convert back and forth between colors and string names.
 """
 
-import re
+import numbers, re
 from . import juce, classic
 
 
@@ -33,7 +33,7 @@ def name_to_color(name):
     try:
         color = to_color(name)
     except:
-        raise ValueError('Unknown color name %s' % name)
+        raise ValueError('Unknown color name %s' % str(name))
 
     if not all(0 <= i <= 255 for i in color):
         raise ValueError('Component out of range: %s' % color)
@@ -49,10 +49,14 @@ def color_to_name(color, use_hex=False):
     ``name_to_color(color_to_name(c)) == c`` is guaranteed to be true (but the
     reverse is not true, because name_to_color is a many-to-one function).
     """
+    if isinstance(color, list):
+        color = tuple(color)
+    elif not isinstance(color, tuple):
+        raise ValueError('Not a color')
+
     if use_hex:
         return '#%02x%02x%02x' % color
-    if not isinstance(color, tuple):
-        color = tuple(color)
+
     try:
         return TO_NAME[color]
     except:
@@ -119,6 +123,30 @@ def _from_number(s):
     return int(s)
 
 
+def to_color(c):
+    """Try to coerce the argument into a color - a 3-tuple of numbers-"""
+    if isinstance(c, numbers.Number):
+        return c, c, c
+
+    if not c:
+        raise ValueError('Cannot create color from empty "%s"' % c)
+
+    if isinstance(c, str):
+        return name_to_color(c)
+
+    if isinstance(c, list):
+        c = tuple(c)
+
+    if isinstance(c, tuple):
+        if len(c) > 3:
+            return c[:3]
+        while len(c) < 3:
+            c += (c[-1],)
+        return c
+
+    raise ValueError('Cannot create color from "%s"' % c)
+
+
 """
 COLOR is a "magic" color name variable.
 


Test patch:
diff --git a/test/bibliopixel/util/colors/__init__.py b/test/bibliopixel/util/colors/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/test/bibliopixel/util/colors/names_test.py b/test/bibliopixel/util/colors/names_test.py
new file mode 100644
index 0000000..dfa7ff2
--- /dev/null
+++ b/test/bibliopixel/util/colors/names_test.py
@@ -0,0 +1,42 @@
+import unittest
+
+from bibliopixel.util import colors
+COLORS = 'red', 'white', 'magenta', 'lime green', '(0, 1, 2)'
+
+
+class NamesTest(unittest.TestCase):
+    def test_round_trip(self):
+        for name in COLORS:
+            color = colors.name_to_color(name)
+            name2 = colors.color_to_name(color)
+            self.assertEqual(name, name2)
+
+    def test_to_color(self):
+        for name in COLORS:
+            color1 = colors.name_to_color(name)
+            color2 = colors.to_color(name)
+            color3 = colors.to_color(color2)
+            self.assertEqual(color1, color2)
+            self.assertEqual(color2, color3)
+
+        self.assertEqual(colors.to_color(0), colors.COLORS.black)
+        self.assertEqual(colors.to_color([255]), colors.COLORS.white)
+        self.assertEqual(colors.to_color((255, 0)), colors.COLORS.red)
+
+    def test_name_to_color_error(self):
+        colors.name_to_color('red')
+        with self.assertRaises(ValueError):
+            colors.name_to_color(3)
+        with self.assertRaises(ValueError):
+            colors.name_to_color('xxx')
+        with self.assertRaises(ValueError):
+            colors.name_to_color((1, 2, 3))
+
+    def test_color_to_name_error(self):
+        with self.assertRaises(ValueError):
+            colors.color_to_name('red')
+        with self.assertRaises(ValueError):
+            colors.color_to_name(3)
+        with self.assertRaises(ValueError):
+            colors.color_to_name('xxx')
+        colors.color_to_name((1, 2, 3))