Commit 4ef55a03 authored by Pietro Saccardi's avatar Pietro Saccardi
Browse files

Improvements to DotDict and settings.

* DotDicts are lazily replaced over dicts when __getattr__ is called.
* DotDicts hierarchy is created lazily, when the value is set, the
  DotDict is stored in the parent object
* Added get method that can sanitize and cast the value
parent 641a07f4
Loading
Loading
Loading
Loading
+70 −14
Original line number Diff line number Diff line
# https://stackoverflow.com/a/13520518/1749822
class DotDict(dict):
    __delattr__ = dict.__delitem__
class DotDict:
    _PASSTHROUGH_MEMBERS = ('_d', '_parent_and_key')

    def __setattr__(self, key, value):
        if isinstance(value, dict):
            value = DotDict(value)
        self[key] = value
    def __delattr__(self, item):
        if item in self._d:
            del self._d[item]

    def __getattr__(self, item):
        return self.get(item, None)
        if item in DotDict._PASSTHROUGH_MEMBERS:
            return super(DotDict, self).__getattr__(item)
        if item in self._d:
            retval = self._d[item]
            if isinstance(retval, dict):
                retval = DotDict(retval)
                self._d[item] = retval
            return retval
        else:
            return DotDict({}, (self, item))

    def __setattr__(self, key, value):
        if key in DotDict._PASSTHROUGH_MEMBERS:
            super(DotDict, self).__setattr__(key, value)
            return
        self._d[key] = value
        if self._parent_and_key is not None:
            setattr(*self._parent_and_key, self._d)
            self._parent_and_key = None

    def __contains__(self, item):
        return item in self._d

    def __iter__(self):
        return iter(self._d)

    def items(self):
        for k in self:
            yield k, getattr(self, k)

    def values(self):
        for k in self:
            yield getattr(self, k)

    def get(self, key, default=None, cast_to_type=None, ge=None, le=None, sanitizer=None):
        if key in self:
            retval = getattr(self, key)
            if cast_to_type is not None:
                # noinspection PyBroadException
                try:
                    retval = cast_to_type(retval)
                except:
                    retval = default
        else:
            retval = default
        if sanitizer is not None:
            retval = sanitizer(retval)
        if retval is not None and ge is not None:
            retval = max(retval, ge)
        if retval is not None and le is not None:
            retval = min(retval, le)
        return retval

    def __init__(self, d, parent_and_key=None):
        self._d = d
        self._parent_and_key = parent_and_key

    def __init__(self, dct):
        super(DotDict, self).__init__(dct)
        for key, value in self.items():
            if isinstance(value, dict):
                self[key] = DotDict(value)
    def _downcast_and_get_d(self):
        for k, v in self._d.items():
            if isinstance(v, DotDict):
                self._d[k] = v._downcast_and_get_d()
        return self._d

    def to_dict_tree(self):
        return dict({k: v.to_dict_tree() if isinstance(v, DotDict) else v for k, v in self.items()})
    def get_storage(self, downcast=True):
        if downcast:
            self._downcast_and_get_d()
        return self._d
+1 −1
Original line number Diff line number Diff line
@@ -40,7 +40,7 @@ def save_settings(path, log=None):
    path = os.path.abspath(path)
    try:
        with open(path, 'w') as fp:
            json.dump(SETTINGS.to_dict_tree(), fp, cls=ExtendedJSONCodec, indent=2)
            json.dump(SETTINGS.get_storage(downcast=True), fp, cls=ExtendedJSONCodec, indent=2)
    except OSError:  # pragma: no cover
        if log:
            log.exception('Could not write to settings file %s.', path)
+23 −1
Original line number Diff line number Diff line
@@ -95,9 +95,31 @@ class TestDotDict(unittest.TestCase):
        self.assertIsNotNone(d.b)
        self.assertIsNotNone(d.b.c)
        self.assertIsInstance(d.b.c, DotDict)
        d = d.to_dict_tree()
        d = d.get_storage()
        self.assertEqual(d, {'a': {}, 'b': {'c': {}}})

    def test_lazy_alloc(self):
        d = DotDict({})
        self.assertIsNone(d.something.get('a'))
        self.assertNotIn('something', d)
        d.something.a = 55
        self.assertIn('something', d)
        self.assertIn('a', d.something)
        self.assertEqual(d.something.a, 55)

    def test_get(self):
        d = DotDict({'a': 55})
        self.assertIsNone(d.get('b'))
        self.assertEqual(d.get('a'), 55)
        self.assertEqual(d.get('b', default=42), 42)
        d.c = '55'
        self.assertEqual(d.get('c', cast_to_type=int), 55)
        d.c = 'string'
        self.assertEqual(d.get('c', cast_to_type=int, default=32), 32)
        self.assertEqual(d.get('a', ge=56), 56)
        self.assertEqual(d.get('a', le=54), 54)
        self.assertEqual(d.get('a', sanitizer=lambda n: 2 * n), 110)


class VideoEventCollector:
    def __init__(self, events):