Repository: ManiacalLabs/BiblioPixel
Pull request #1003 (2018-10-11 16:32:48)

Extract `bp-color`
Clean up a stray script, test some stuff, prepare for a more modularized `bp`.


Patch:
diff --git a/bibliopixel/main/color.py b/bibliopixel/main/color.py
deleted file mode 100644
index 8c606a1..0000000
--- a/bibliopixel/main/color.py
+++ /dev/null
@@ -1,37 +0,0 @@
-"""
-Toggle between color names and color tuples
-"""
-
-from .. util import colors, log
-
-
-def run(args):
-    if args.list:
-        log.printer(*sorted(colors.COLOR_DICT.items()), sep='\n')
-
-    if not args.colors:
-        if not args.list:
-            raise ValueError('No colors supplied!')
-
-    failures = []
-    for c in args.colors:
-        try:
-            log.printer(c, colors.toggle(c), sep=': ')
-        except Exception as e:
-            failures.append(c)
-    if failures:
-        s = '' if len(failures) == 1 else 's'
-        failures = ', '.join('"%s"' % f for f in failures)
-        raise ValueError('Did not understand color%s %s' % (s, failures))
-
-
-def set_parser(parser):
-    parser.set_defaults(run=run)
-    parser.add_argument(
-        'colors', nargs='*',
-        help='Color names or tuples',
-        default='')
-
-    parser.add_argument(
-        '-l', '--list', action='store_true',
-        help='List all the colors')
diff --git a/bibliopixel/main/commands.py b/bibliopixel/main/commands.py
index 3302504..711891d 100644
--- a/bibliopixel/main/commands.py
+++ b/bibliopixel/main/commands.py
@@ -3,7 +3,7 @@ from .. project.importer import import_module
 from .. util import deprecated, log
 
 COMMANDS = (
-    'animations', 'all_pixel', 'all_pixel_test', 'clear_cache', 'color', 'demo',
+    'animations', 'all_pixel', 'all_pixel_test', 'clear_cache', 'demo',
     'devices', 'info', 'monitor', 'new', 'run')
 
 if deprecated.allowed():  # pragma: no cover
diff --git a/bibliopixel/util/colors/closest_colors.py b/bibliopixel/util/colors/closest_colors.py
new file mode 100644
index 0000000..aafae93
--- /dev/null
+++ b/bibliopixel/util/colors/closest_colors.py
@@ -0,0 +1,31 @@
+import itertools
+from . import names
+
+
+def closest_colors(color_name, metric=None):
+    metric = metric or euclidean
+    color = names.to_color(color_name)
+
+    mn_list = [(metric(color, c), n) for n, c in names.COLOR_DICT.items()]
+    mn_list.sort()
+    min_metric = mn_list[0][0]
+    result = []
+    for met, name in mn_list:
+        if met > min_metric:
+            return sorted(result)
+        result.append(name)
+
+
+def euclidean(c1, c2):
+    """Square of the euclidean distance"""
+    diffs = ((i - j) for i, j in zip(c1, c2))
+    return sum(x * x for x in diffs)
+
+
+def taxicab(c1, c2):
+    diffs = ((i - j) for i, j in zip(c1, c2))
+    return sum(abs(x) for x in diffs)
+
+
+def all_colors(start=0, stop=255, skip=1):
+    return itertools.product(range(start, stop, skip), repeat=3)
diff --git a/scripts/bp-color b/scripts/bp-color
new file mode 100755
index 0000000..932b37a
--- /dev/null
+++ b/scripts/bp-color
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+
+import argparse, sys
+from bibliopixel.util.colors.closest_colors import closest_colors
+from bibliopixel.util import colors, log
+
+HELPS = {'help', '-h', '--h', '--help', '--h'}
+USAGE = """Usage:
+  bp-color color [color color ...]
+
+Toggle between color names and color tuples
+
+with --closest, prints the nearest named color.
+"""
+
+
+def main():
+    run(make_args())
+
+
+def make_args():
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        'colors', nargs='*',
+        help='Color names or tuples',
+        default='')
+
+    parser.add_argument(
+        '-l', '--list', action='store_true',
+        help='List all the colors')
+
+    parser.add_argument(
+        '-c', '--closest', action='store_true',
+        help='List the closest named colors')
+    return parser.parse_args(sys.argv[1:])
+
+
+def run(args):
+    if args.list:
+        log.printer(*sorted(colors.COLOR_DICT.items()), sep='\n')
+
+    if not args.colors:
+        if not args.list:
+            raise ValueError('No colors supplied!')
+
+    failures = []
+    for c in args.colors:
+        try:
+            if args.closest:
+                color = colors.names.to_color(c)
+                closest = ('"%s"' % c for c in closest_colors(color))
+                log.printer(c, end=': ')
+                log.printer(*closest, sep=', ')
+            else:
+                log.printer(c, colors.toggle(c), sep=': ')
+        except Exception as e:
+            failures.append(c)
+    if failures:
+        s = '' if len(failures) == 1 else 's'
+        failures = ', '.join('"%s"' % f for f in failures)
+        raise ValueError('Did not understand color%s %s' % (s, failures))
+
+
+if __name__ == '__main__':
+    main()
diff --git a/scripts/nearest-color b/scripts/nearest-color
deleted file mode 100755
index 4e3d9e4..0000000
--- a/scripts/nearest-color
+++ /dev/null
@@ -1,47 +0,0 @@
-#!/usr/bin/env python3
-
-"""
-This minor utility is useful for understanding hex color patterns by
-printing the closest color name for any input hex color number.
-"""
-
-import sys
-from bibliopixel.util.colors import names
-from bibliopixel.util import log
-
-HELPS = {'help', '-h', '--h', '--help', '--h'}
-USAGE = """Usage:
-  nearest-color color [color color ...]
-
-Given a hex color, prints the nearest named color.
-"""
-
-def main(*colors):
-    if not colors:
-        log.printer(USAGE)
-
-    for color in colors:
-        if color in HELPS:
-            log.printer(USAGE)
-        else:
-            closest_color(color)
-
-
-def taxicab_metric(c1, c2):
-    return sum(abs(i - j) for i, j in zip(c1, c2))
-
-
-def closest_color(color_name, metric=taxicab_metric):
-    color = names.to_color(color_name)
-    m = min(metric(color, c) for c in names.COLOR_DICT.values())
-    closest = sorted(k for k, v in names.COLOR_DICT.items()
-                     if metric(color, v) == m)
-
-    print(color_name, ': "', '", "'.join(closest), '"', sep='')
-
-    s = sorted((metric(color, v), k) for k, v in names.COLOR_DICT.items())
-    # print(s, sep='\n')
-
-
-if __name__ == '__main__':
-    main(*sys.argv[1:])
diff --git a/setup.py b/setup.py
index bc966db..fcb111e 100755
--- a/setup.py
+++ b/setup.py
@@ -4,43 +4,10 @@ from setuptools.command.install_scripts import install_scripts
 from setuptools.command.install import install as _install
 import os, platform, sys
 
-
-INSTALLATION_ERROR = """INSTALLATION ERROR!
-
-BiblioPixel v3 requires Python 3.4+ but
-you are using version {0.major}.{0.minor}.{0.micro}
-
-If you absolutely require using Python 2,
-please install BiblioPixel v2.x using:
-
-    > pip install "bibliopixel<3.0"
-
-However we highly recommend using the latest BiblioPixel
-(v3+) with Python 3.4+.
-"""
-
-BAT_TEMPLATE = \
-    r"""@echo off
-REM wrapper to use shebang first line of {FNAME}
-
-set mypath=%~dp0
-set pyscript="%mypath%{FNAME}"
-set /p line1=<%pyscript%
-
-if "%line1:~0,2%" == "#!" (goto :goodstart)
-echo First line of %pyscript% does not start with "#!"
-exit /b 1
-
-:goodstart
-set py_exe=%line1:~2%
-call "%py_exe%" %pyscript% %*
-"""
-
-NO_PYTHON_ERROR = """
-WARNING: No #!python executable found in %s, skipping .bat wrapper'"""
-
 printer = print  # noqa: T001
 
+SCRIPTS_TO_INSTALL = 'bp', 'bibliopixel', 'bp-color', 'bp-pid'
+
 
 class InstallScripts(install_scripts):
     def run(self):
@@ -116,6 +83,21 @@ def _get_version():
     return open(filename).read().strip()
 
 
+INSTALLATION_ERROR = """INSTALLATION ERROR!
+
+BiblioPixel v3 requires Python 3.4+ but
+you are using version {0.major}.{0.minor}.{0.micro}
+
+If you absolutely require using Python 2,
+please install BiblioPixel v2.x using:
+
+    > pip install "bibliopixel<3.0"
+
+However we highly recommend using the latest BiblioPixel
+(v3+) with Python 3.4+.
+"""
+
+
 if sys.version_info.major != 3:
     printer(INSTALLATION_ERROR.format(sys.version_info))
     sys.exit(1)
@@ -153,6 +135,26 @@ setup(
         'install_scripts': InstallScripts
     },
     include_package_data=True,
-    scripts=['scripts/bp', 'scripts/bp-pid', 'scripts/bibliopixel'],
+    scripts=['scripts/' + s for s in SCRIPTS_TO_INSTALL],
     install_requires=REQUIRED
 )
+
+BAT_TEMPLATE = \
+    r"""@echo off
+REM wrapper to use shebang first line of {FNAME}
+
+set mypath=%~dp0
+set pyscript="%mypath%{FNAME}"
+set /p line1=<%pyscript%
+
+if "%line1:~0,2%" == "#!" (goto :goodstart)
+echo First line of %pyscript% does not start with "#!"
+exit /b 1
+
+:goodstart
+set py_exe=%line1:~2%
+call "%py_exe%" %pyscript% %*
+"""
+
+NO_PYTHON_ERROR = """
+WARNING: No #!python executable found in %s, skipping .bat wrapper'"""


Test patch:
diff --git a/test/bibliopixel/util/colors/closest_colors_test.py b/test/bibliopixel/util/colors/closest_colors_test.py
new file mode 100644
index 0000000..ef6d2f1
--- /dev/null
+++ b/test/bibliopixel/util/colors/closest_colors_test.py
@@ -0,0 +1,31 @@
+import collections, unittest
+
+from bibliopixel.util.colors import closest_colors
+
+closest = closest_colors.closest_colors
+
+
+class ClosestColorsTest(unittest.TestCase):
+    def exhaustive(self, metric, start=32, skip=64, report=4):
+        for color in closest_colors.all_colors(start, skip=skip):
+            cl = closest(color, metric)
+            if len(cl) >= report:
+                yield color, cl
+
+    def test_simple(self):
+        self.assertEqual(closest((255, 0, 0)), ['red', 'red 1'])
+
+    def test_far(self):
+        c = closest((0, 0, 64), metric=closest_colors.taxicab)
+        self.assertEqual(c, ['black', 'navy', 'none', 'off'])
+
+        c = closest((64, 0, 0), metric=closest_colors.taxicab)
+        self.assertEqual(c, ['black', 'maroon', 'none', 'off'])
+
+    def test_euclidean(self):
+        ex = list(self.exhaustive(closest_colors.euclidean))
+        self.assertEqual(ex, [])
+
+    def test_taxicab(self):
+        ex = list(self.exhaustive(closest_colors.taxicab))
+        self.assertEqual(ex, [])