From 2a048268828cd9d457fa3a48eb668c3f74a38fe6 Mon Sep 17 00:00:00 2001 From: Gregory Szorc Date: Tue, 15 Jan 2013 22:21:18 -0800 Subject: [PATCH] Bug 784841 - Part 1: Create generic container classes; r=jhammel We create some specialized dicts that will be used in later patches. --- .../mozbuild/mozbuild/test/test_containers.py | 104 ++++++++++++++++++ python/mozbuild/mozbuild/util.py | 57 ++++++++++ 2 files changed, 161 insertions(+) create mode 100644 python/mozbuild/mozbuild/test/test_containers.py diff --git a/python/mozbuild/mozbuild/test/test_containers.py b/python/mozbuild/mozbuild/test/test_containers.py new file mode 100644 index 0000000000000..f2a1e2da793b5 --- /dev/null +++ b/python/mozbuild/mozbuild/test/test_containers.py @@ -0,0 +1,104 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +import unittest + +from mozunit import main + +from mozbuild.util import ( + DefaultOnReadDict, + ReadOnlyDefaultDict, + ReadOnlyDict, +) + +class TestReadOnlyDict(unittest.TestCase): + def test_basic(self): + original = {'foo': 1, 'bar': 2} + + test = ReadOnlyDict(original) + + self.assertEqual(original, test) + self.assertEqual(test['foo'], 1) + + with self.assertRaises(KeyError): + value = test['missing'] + + with self.assertRaises(Exception): + test['baz'] = True + +class TestDefaultOnReadDict(unittest.TestCase): + def test_no_defaults(self): + original = {'foo': 1, 'bar': 2} + + test = DefaultOnReadDict(original) + self.assertEqual(original, test) + + with self.assertRaises(KeyError): + value = test['missing'] + + test['foo'] = 5 + self.assertEqual(test['foo'], 5) + + def test_dict_defaults(self): + original = {'foo': 1, 'bar': 2} + + test = DefaultOnReadDict(original, defaults={'baz': 3}) + + self.assertEqual(original, test) + self.assertEqual(test['baz'], 3) + + with self.assertRaises(KeyError): + value = test['missing'] + + test['baz'] = 4 + self.assertEqual(test['baz'], 4) + + def test_global_default(self): + original = {'foo': 1} + + test = DefaultOnReadDict(original, defaults={'bar': 2}, + global_default=10) + + self.assertEqual(original, test) + self.assertEqual(test['foo'], 1) + + self.assertEqual(test['bar'], 2) + self.assertEqual(test['baz'], 10) + + test['bar'] = 3 + test['baz'] = 12 + test['other'] = 11 + + self.assertEqual(test['bar'], 3) + self.assertEqual(test['baz'], 12) + self.assertEqual(test['other'], 11) + + +class TestReadOnlyDefaultDict(unittest.TestCase): + def test_simple(self): + original = {'foo': 1, 'bar': 2} + + test = ReadOnlyDefaultDict(original) + + self.assertEqual(original, test) + + self.assertEqual(test['foo'], 1) + + with self.assertRaises(KeyError): + value = test['missing'] + + def test_assignment(self): + test = ReadOnlyDefaultDict({}) + + with self.assertRaises(Exception): + test['foo'] = True + + def test_defaults(self): + test = ReadOnlyDefaultDict({}, defaults={'foo': 1}) + + self.assertEqual(test['foo'], 1) + + +if __name__ == '__main__': + main() diff --git a/python/mozbuild/mozbuild/util.py b/python/mozbuild/mozbuild/util.py index e8fcfc4c4e46d..51e52bb7f7a15 100644 --- a/python/mozbuild/mozbuild/util.py +++ b/python/mozbuild/mozbuild/util.py @@ -7,6 +7,7 @@ from __future__ import unicode_literals +import copy import hashlib @@ -27,3 +28,59 @@ def hash_file(path): h.update(data) return h.hexdigest() + + +class ReadOnlyDict(dict): + """A read-only dictionary.""" + def __init__(self, d): + dict.__init__(self, d) + + def __setitem__(self, name, value): + raise Exception('Object does not support assignment.') + + +class undefined_default(object): + """Represents an undefined argument value that isn't None.""" + + +undefined = undefined_default() + + +class DefaultOnReadDict(dict): + """A dictionary that returns default values for missing keys on read.""" + + def __init__(self, d, defaults=None, global_default=undefined): + """Create an instance from an iterable with defaults. + + The first argument is fed into the dict constructor. + + defaults is a dict mapping keys to their default values. + + global_default is the default value for *all* missing keys. If it isn't + specified, no default value for keys not in defaults will be used and + IndexError will be raised on access. + """ + dict.__init__(self, d) + + self._defaults = defaults or {} + self._global_default = global_default + + def __getitem__(self, k): + try: + return dict.__getitem__(self, k) + except: + pass + + if k in self._defaults: + dict.__setitem__(self, k, copy.deepcopy(self._defaults[k])) + elif self._global_default != undefined: + dict.__setitem__(self, k, copy.deepcopy(self._global_default)) + + return dict.__getitem__(self, k) + + +class ReadOnlyDefaultDict(DefaultOnReadDict, ReadOnlyDict): + """A read-only dictionary that supports default values on retrieval.""" + def __init__(self, d, defaults=None, global_default=undefined): + DefaultOnReadDict.__init__(self, d, defaults, global_default) +