Repository: ManiacalLabs/BiblioPixel
Pull request #141 (2017-03-25 20:40:18)

set and show commands to main.
Getting closer...


Patch:
diff --git a/bibliopixel/main/demo.py b/bibliopixel/main/demo.py
index e02c579..115f2ba 100644
--- a/bibliopixel/main/demo.py
+++ b/bibliopixel/main/demo.py
@@ -49,7 +49,7 @@ DEMO_TABLE = {
 }
 
 
-def run(args):
+def run(args, settings):
     try:
         demo = DEMO_TABLE[args.name]
     except KeyError:
diff --git a/bibliopixel/main/main.py b/bibliopixel/main/main.py
index 376a37b..ae21e22 100644
--- a/bibliopixel/main/main.py
+++ b/bibliopixel/main/main.py
@@ -1,13 +1,15 @@
-import argparse, sys
+import argparse, os, sys
 from .. util.importer import import_symbol
+from .. project.settings_file import SettingsFile
 
 __all__ = ['main']
 
-COMMANDS = ('demo', 'run',)  # 'device', 'network', 'settings'
+COMMANDS = ('demo', 'run', 'set', 'show',)  # 'network'
 MODULES = {c: import_symbol('.' + c, 'bibliopixel.main') for c in COMMANDS}
+DOTFILE_DEFAULT = '~/.bibliopixel'
 
 
-def no_command(_):
+def no_command(*_):
     print('ERROR: No command entered')
     print('Valid:', *COMMANDS)
     return -1
@@ -17,10 +19,12 @@ def main():
     parser = argparse.ArgumentParser()
     subparsers = parser.add_subparsers()
     for name, module in MODULES.items():
-        # TODO: we should add help text in these two steps.
         subparser = subparsers.add_parser(name, help=module.HELP)
         module.set_parser(subparser)
+    parser.add_argument('--settings',
+                        help='Filename for settings', default=DOTFILE_DEFAULT)
 
     args = parser.parse_args()
+    settings = SettingsFile(os.path.expanduser(args.settings), True)
     run = getattr(args, 'run', no_command)
-    sys.exit(run(args) or 0)
+    sys.exit(run(args, settings) or 0)
diff --git a/bibliopixel/main/run.py b/bibliopixel/main/run.py
index 50ed58c..362663c 100644
--- a/bibliopixel/main/run.py
+++ b/bibliopixel/main/run.py
@@ -5,7 +5,7 @@ Run a project description file.
 """
 
 
-def run(args):
+def run(args, settings):
     data = args.name and open(args.name).read() or {}
     project.run(data)
 
diff --git a/bibliopixel/main/set.py b/bibliopixel/main/set.py
new file mode 100644
index 0000000..b220507
--- /dev/null
+++ b/bibliopixel/main/set.py
@@ -0,0 +1,48 @@
+import json
+from ..project.defaults import SECTIONS
+
+HELP = """
+Set bibliopixel values
+"""
+
+
+def run(args, settings):
+    parts = args.name.split('.')
+
+    if len(parts) == 2:
+        parts.append('')
+
+    elif len(parts) != 3:
+        print('ERROR: name must have 2 or 3 parts: %s' % args.name)
+        return -1
+
+    section, name, key = parts
+
+    if section not in SECTIONS:
+        print('ERROR: no section %s' % section)
+        return -1
+
+    sdict = settings.data.setdefault(section, {})
+
+    if key:
+        sdict.setdefault(name, {})[key] = args.value
+    else:
+        try:
+            value = json.loads(args.value)
+        except:
+            print('ERROR: Can\'t parse json value: %s' % args.value)
+            return -1
+
+        if not isinstance(value, dict):
+            print('ERROR: value must be a dictionary: %s' % args.value)
+            return -1
+
+        sdict[name] = value
+
+    settings.write()
+
+
+def set_parser(parser):
+    parser.set_defaults(run=run)
+    parser.add_argument('name')
+    parser.add_argument('value')
diff --git a/bibliopixel/main/show.py b/bibliopixel/main/show.py
new file mode 100644
index 0000000..b25b063
--- /dev/null
+++ b/bibliopixel/main/show.py
@@ -0,0 +1,77 @@
+from ..project.defaults import SECTIONS
+
+HELP = """
+Show bibliopixel settings.
+"""
+
+
+def run(args, settings):
+    def show_all():
+        if not settings.data:
+            print('No settings.')
+            return
+
+        first = True
+        for name, section in sorted(settings.data.items()):
+            if first:
+                first = False
+            else:
+                print()
+
+            show_section(name, section)
+
+    def show_section(name, section):
+        print('%s:' % name)
+        for i in sorted(section.keys()):
+            print('    %s' % i)
+
+    def show_setting(setting):
+        for k, v in sorted(setting.items()):
+            print('%s = %s' % (k, v))
+
+    if not args.name:
+        show_all()
+        return
+
+    parts = args.name.split('.')
+    section_name = parts.pop(0)
+
+    try:
+        section = settings.data[section_name]
+    except KeyError:
+        print('ERROR: no section %s' % section_name)
+        return -1
+
+    if not parts:
+        show_section(section_name, section)
+        return
+
+    name = parts.pop(0)
+    try:
+        setting = section[name]
+    except KeyError:
+        print('ERROR: no setting %s in section %s' % (section_name, section))
+        return -1
+
+    if not parts:
+        show_setting(setting)
+        return
+
+    key = parts.pop(0)
+    if parts:
+        print('ERROR: too many segments in name %s' % args.name)
+        return -1
+
+    try:
+        value = setting[args.key]
+    except:
+        print('ERROR: no key %s in setting %s in section %s' %
+              (key, setting, section_name))
+        return -1
+
+    print(value)
+
+
+def set_parser(parser):
+    parser.set_defaults(run=run)
+    parser.add_argument('name', nargs='?', default='')
diff --git a/bibliopixel/project/defaults.py b/bibliopixel/project/defaults.py
index 113faf0..8cbf4f1 100644
--- a/bibliopixel/project/defaults.py
+++ b/bibliopixel/project/defaults.py
@@ -20,6 +20,8 @@ DEFAULTS = {
     'run': {},
 }
 
+SECTIONS = tuple(sorted(DEFAULTS.keys()))
+
 
 def apply_defaults(desc):
     result = copy.deepcopy(DEFAULTS)
diff --git a/bibliopixel/project/settings_file.py b/bibliopixel/project/settings_file.py
new file mode 100644
index 0000000..6414b0e
--- /dev/null
+++ b/bibliopixel/project/settings_file.py
@@ -0,0 +1,56 @@
+import copy
+
+from ..util.datafile import DataFile
+
+"""
+The BP settings file is a dotfile with four sections:
+
+    driver, led, animation, run.
+
+Each section is a dictionary mapping setting names to settings dictionaries.
+
+The setting named "default" is used if none is specified, or the system default
+if no setting named "default" exists.
+
+If a settings dictionary has the entry "extends" then it extends and overrides
+another setting in the same dictionary.  The extends feature is transitive
+but cannot be used recursively.
+"""
+
+
+def get_setting(section, name):
+    extensions = []
+
+    while name:
+        setting = section[name]
+        extensions.append(setting)
+        name = setting.get('extends')
+
+    setting = {}
+    for s in reversed(extensions):
+        setting.update(s)
+
+    try:
+        del setting['extends']
+    except KeyError:
+        pass
+    return setting
+
+
+class SettingsFile(DataFile):
+    def _section(self, section):
+        return self.data.setdefault(section, {})
+
+    def get_setting(self, section, name):
+        return get_setting(self._section(section), name)
+
+    def get_value(self, section, name, key):
+        return self.get_setting(section, name)[key]
+
+    def set_setting(self, section, name, setting):
+        self._section(section)[name] = setting
+        self.write()
+
+    def set_value(self, section, name, key, value):
+        self._section(section).set_default(name, {})[key] = value
+        self.write()
diff --git a/bibliopixel/util/datafile.py b/bibliopixel/util/datafile.py
index 7b8eb12..e37b45e 100644
--- a/bibliopixel/util/datafile.py
+++ b/bibliopixel/util/datafile.py
@@ -1,16 +1,16 @@
 import configparser, json
 
 
-def read_config(fp):
+def load_config(fp):
     interpolation = configparser.ExtendedInterpolation()
     parser = configparser.ConfigParser(interpolation=interpolation)
 
     parser.read_file(fp)
 
-    return {k: dict(v) for k, v in parser.items()}
+    return {k: dict(v) for k, v in parser.items() if dict(v)}
 
 
-def write_config(data, fp):
+def dump_config(data, fp):
     parser = configparser.ConfigParser()
     for k, v in data.items():
         parser[k] = v
@@ -18,21 +18,26 @@ def write_config(data, fp):
     parser.write(fp)
 
 
-def reader_writer(filename, is_json=None, open=open):
-    if is_json is None:
-        is_json = filename.endswith('.json')
+class DataFile(object):
+    def __init__(self, filename, is_json=None, open=open):
+        self.filename = filename
 
-    read = json.load if is_json else read_config
-    write = json.dump if is_json else write_config
+        if is_json is None:
+            is_json = filename.endswith('.json')
 
-    def reader():
+        self.load = json.load if is_json else load_config
+        self.dump = json.dump if is_json else dump_config
+        self.open = open
+        self.data = {}
+        self.read()
+
+    def read(self):
         try:
-            fp = open(filename)
+            fp = self.open(self.filename)
         except FileNotFoundError:
-            return {}
-        return read(fp)
-
-    def writer(data):
-        return write(data, open(filename, 'w'))
+            self.data = {}
+        else:
+            self.data = self.load(fp)
 
-    return reader, writer
+    def write(self):
+        self.dump(self.data, self.open(self.filename, 'w'))


Test patch:
diff --git a/test/all_test.py b/test/all_test.py
index 2cdf564..3711144 100644
--- a/test/all_test.py
+++ b/test/all_test.py
@@ -1,6 +1,6 @@
 import unittest
-
 import bibliopixel
+from bibliopixel.main import demo, main, run, set, show
 
 from bibliopixel import (
     animation,
diff --git a/test/datafile_test.py b/test/datafile_test.py
index 90e148a..4e725f2 100644
--- a/test/datafile_test.py
+++ b/test/datafile_test.py
@@ -1,7 +1,7 @@
 import io, json, unittest
 from .mock import mock_open
 
-from bibliopixel.util import datafile
+from bibliopixel.util.datafile import load_config, DataFile
 
 DEFAULTS = {
     'a': {'foo': 0, 'bang': 0, 'burt': 0},
@@ -26,25 +26,25 @@ class DatafileTest(unittest.TestCase):
     def test_reader_writer_json(self):
         context = {'test.json': JSON_TEST}
         mopen = mock_open(context)
-        reader, writer = datafile.reader_writer('test.json', open=mopen)
-
-        self.assertEqual(reader(), {'a': {'foo': 'bar', 'bang': 1}})
-        d = {'bang': {'hi': 'there'}}
-        writer(d)
-        self.assertEqual(json.loads(context['test.json']), d)
+        datafile = DataFile('test.json', open=mopen)
+        datafile.read()
+        self.assertEqual(datafile.data, {'a': {'foo': 'bar', 'bang': 1}})
+        datafile.data = {'bang': {'hi': 'there'}}
+        datafile.write()
+        self.assertEqual(json.loads(context['test.json']), datafile.data)
 
     def test_reader_writer_config(self):
         context = {'.test': CONFIG_TEST}
         mopen = mock_open(context)
-        reader, writer = datafile.reader_writer('.test', open=mopen)
-        result = {
-            'DEFAULT': {},
-            'a': {'foo': 'bar', 'bang': '1'},
-        }
-        self.assertEqual(reader(), result)
-
-        d = {'bang': {'hi': 'there'}}
-        writer(d)
+        datafile = DataFile('.test', open=mopen)
+        datafile.read()
+
+        result = {'a': {'foo': 'bar', 'bang': '1'}}
+        self.assertEqual(datafile.data, result)
+
+        datafile.data = {'bang': {'hi': 'there'}}
+        datafile.write()
+
         fp = io.StringIO(context['.test'])
-        config = datafile.read_config(fp)
-        self.assertEqual(config, dict(d, DEFAULT={}))
+        config = load_config(fp)
+        self.assertEqual(config, datafile.data)
diff --git a/test/settings_test.py b/test/settings_test.py
new file mode 100644
index 0000000..33f1c97
--- /dev/null
+++ b/test/settings_test.py
@@ -0,0 +1,16 @@
+import unittest
+from bibliopixel.project.settings_file import get_setting
+
+
+class SettingTest(unittest.TestCase):
+    def test_all(self):
+        section = {
+            'a': {'foo': 1, 'bar': 2},
+            'b': {'extends': 'a', 'bar': 3},
+            'c': {'extends': 'b', 'bar': 4, 'foo': 2, 'baz': 7},
+        }
+
+        self.assertEqual(get_setting(section, 'a'), {'foo': 1, 'bar': 2})
+        self.assertEqual(get_setting(section, 'b'), {'foo': 1, 'bar': 3})
+        self.assertEqual(get_setting(section, 'c'),
+                         {'foo': 2, 'bar': 4, 'baz': 7})