Repository: ManiacalLabs/BiblioPixel (python)

Issue:
Get rid of "user defaults" concept
It's really not that helpful, it allows you to shoot yourself in the foot, and I don't want to document it.

I'm 98% that I'm the only one using it.

If you used that functionality, you could explicitly name the default file you were using instead...

That would involve deleting these `bp` commands:

* list
* load
* remove
* reset
* save
* set
* show

 

Pull request description:
 New `bp animations` command lists all the known animations
* There are 97 animations right now
* Once we fix #905, I'll rename this to `bp list`

Patch:
diff --git a/bibliopixel/main/animations.py b/bibliopixel/main/animations.py
new file mode 100644
index 0000000..9df532f
--- /dev/null
+++ b/bibliopixel/main/animations.py
@@ -0,0 +1,116 @@
+"""
+List all animations
+"""
+
+import os, bibliopixel, inspect, BiblioPixelAnimations
+from bibliopixel import animation
+from bibliopixel.animation import (
+    collection, indexed, parallel, receiver, wrapper)
+from bibliopixel.util import log, walk
+from bibliopixel.project import importer
+
+
+def run(args):
+    animations = {}
+    for root, filename in walk.walk(ROOTS):
+        basepath = os.path.dirname(root)
+        relpath = os.path.relpath(filename, basepath)
+        for category, name in _get_classes(relpath):
+            animations.setdefault(category, []).append(name)
+
+    count = 0
+    for category, entries in sorted(animations.items()):
+        log.printer(category + ':')
+        for e in sorted(entries):
+            if e not in _BLACKLIST:
+                log.printer(' ', _canonicalize(e))
+                count += 1
+        log.printer()
+
+    log.printer('Total animation classes', count)
+
+
+def set_parser(parser):
+    parser.set_defaults(run=run)
+
+
+def _animation_category(value):
+    if not (inspect.isclass(value) and issubclass(value, animation.Animation)):
+        return
+
+    for cls_name, cls in BASE_CLASSES:
+        if value is cls or value is animation.Animation:
+            return
+
+        if issubclass(value, cls):
+            return cls_name
+    return 'all'
+
+
+def _get_classes(filename):
+    if not filename.endswith('.py'):
+        return
+
+    filename = filename[:-3]
+    path = []
+
+    while filename:
+        filename, segment = os.path.split(filename)
+        path.insert(0, segment)
+
+    if not path or path[-1].startswith('_'):
+        return
+
+    python_path = '.'.join(path)
+
+    try:
+        module = importer.import_module(python_path)
+    except:
+        return
+
+    python_path += '.'
+    for name, value in vars(module).items():
+        category = _animation_category(value)
+        if category and not name.startswith('_'):
+            yield category, python_path + name
+
+
+def _canonicalize(name):
+    parts = name.split('.')
+    p = parts[-1].lower().replace('_', '')
+    q = parts[-2].lower().replace('_', '')
+
+    if p == q:
+        parts.pop()
+
+    name = '.'.join(parts)
+    name = name.replace('bibliopixel.animation.', '.')
+    name = name.replace('BiblioPixelAnimations.', '$bpa.')
+    return name
+
+
+BP_ROOT = os.path.dirname(bibliopixel.__file__)
+BPA_ROOT = os.path.dirname(BiblioPixelAnimations.__file__)
+ROOTS = BP_ROOT, BPA_ROOT
+BASE_CLASSES = (
+    ('circle', animation.Circle),
+    ('cube', animation.BaseCubeAnim),
+    ('game', animation.BaseGameAnim),
+    ('matrix', animation.BaseMatrixAnim),
+    ('strip', animation.BaseStripAnim),
+    ('parallel', parallel.Parallel),
+    ('wrapper', wrapper.Wrapper),
+    ('indexed', indexed.Indexed),
+    ('collection', collection.Collection),
+    ('receiver', receiver.BaseReceiver),
+    ('simple', animation.Animation),
+)
+
+_BLACKLIST = set((
+    'bibliopixel.animation.off.OffAnim',
+    'bibliopixel.animation.remote.control.Off',
+    'bibliopixel.main.demo_table.Sequence',
+    'bibliopixel.main.all_pixel_test.StripChannelTest',
+    'bibliopixel.animation.failed.Failed',
+    'BiblioPixelAnimations.simple.Fill.Fill',
+))
diff --git a/bibliopixel/main/commands.py b/bibliopixel/main/commands.py
index 2f707dc..5b704f7 100644
--- a/bibliopixel/main/commands.py
+++ b/bibliopixel/main/commands.py
@@ -3,6 +3,7 @@ from .. project.importer import import_module
 from .. util import log
 
 COMMANDS = (
+    'animations',
     'all_pixel', 'all_pixel_test', 'clear_cache', 'color', 'demo', 'devices',
     'info', 'kill', 'list', 'load', 'monitor', 'new', 'pid', 'remove', 'reset',
     'restart', 'run', 'save', 'set', 'show', 'shutdown', 'update')
diff --git a/bibliopixel/project/merge.py b/bibliopixel/project/merge.py
index e367e9d..bca7d3c 100644
--- a/bibliopixel/project/merge.py
+++ b/bibliopixel/project/merge.py
@@ -6,7 +6,7 @@ from . import construct, load
 # For example, animation is at the bottom which are  dependent on run and
 # layout, which depend on drivers, and everything depends on path and typename.
 DEFAULT_PROJECT = collections.OrderedDict((
-    ('aliases', {}),
+    ('aliases', {'bpa': 'BiblioPixelAnimations'}),
     ('path', ''),
     ('typename', 'bibliopixel.project.project.Project'),
     ('numbers', 'python'),
diff --git a/bibliopixel/util/walk.py b/bibliopixel/util/walk.py
new file mode 100644
index 0000000..d747af6
--- /dev/null
+++ b/bibliopixel/util/walk.py
@@ -0,0 +1,17 @@
+import os
+
+
+def walk(roots):
+    if isinstance(roots, str):
+        roots = [roots]
+
+    for root in roots:
+        for dirpath, dirnames, filenames in os.walk(root):
+            for f in filenames:
+                yield root, os.path.join(dirpath, f)
+
+
+def walk_suffix(roots, suffixes):
+    for root, filename in walk(roots):
+        if any(filename.endswith(s) for s in suffixes):
+            yield root, filename
diff --git a/scripts/gif-extractor b/scripts/gif-extractor
index 19f4ca0..5117ca2 100755
--- a/scripts/gif-extractor
+++ b/scripts/gif-extractor
@@ -2,7 +2,7 @@
 
 import argparse, os, subprocess, sys, tempfile
 from unittest import mock
-from bibliopixel.util import data_file, log
+from bibliopixel.util import data_file, log, walk
 from bibliopixel.util.image.extract_gif_lines import extract_gif_lines
 from bibliopixel.project import project
 
@@ -10,14 +10,6 @@ SUFFIXES = '.md', '.py'
 RAISE = True
 
 
-def walk(roots):
-    for root in roots:
-        for dirpath, dirnames, filenames in os.walk(root):
-            for f in filenames:
-                if any(f.endswith(s) for s in SUFFIXES):
-                    yield root, os.path.join(dirpath, f)
-
-
 def extract_one_gif(args, filename, desc):
     os.makedirs(os.path.dirname(filename), exist_ok=True)
     default = {
@@ -42,7 +34,7 @@ def extract_one_gif(args, filename, desc):
 
 
 def extract(args):
-    for root, input_file in walk(args.roots):
+    for root, input_file in walk.walk_suffix(args.roots, SUFFIXES):
         relpath = os.path.relpath(input_file, root)
         for filename, desc in extract_gif_lines(relpath, open(input_file)):
             extract_one_gif(args, filename, desc)


Test patch:
diff --git a/test/bibliopixel/project/fill_test.py b/test/bibliopixel/project/fill_test.py
index 594d3e2..2729191 100644
--- a/test/bibliopixel/project/fill_test.py
+++ b/test/bibliopixel/project/fill_test.py
@@ -57,7 +57,7 @@ class FillTest(unittest.TestCase):
         }
         actual = fill_after(copy.deepcopy(source))
         expected = {
-            'aliases': {},
+            'aliases': {'bpa': 'BiblioPixelAnimations'},
             'shape': (),
             'driver': {},
             'drivers': [],
diff --git a/test/bibliopixel/project/merge_test.py b/test/bibliopixel/project/merge_test.py
index 2e4195a..e56d9d6 100644
--- a/test/bibliopixel/project/merge_test.py
+++ b/test/bibliopixel/project/merge_test.py
@@ -29,7 +29,7 @@ class MergeTest(unittest.TestCase):
                 'path': 'path/to/dir'
             })
         expected = {
-            'aliases': {},
+            'aliases': {'bpa': 'BiblioPixelAnimations'},
             'animation': {'typename': 'bfoo'},
             'controls': [],
             'shape': (),