From 1001d393fe6012ef5c0a2d1cc7744fd5f240aea6 Mon Sep 17 00:00:00 2001 From: termie Date: Mon, 8 Oct 2012 16:14:32 -0700 Subject: switch to a dict-based object --- test/test_core.py | 52 +++++++++++++++++++++++++++++++++++++++++++++++++ warlock/core.py | 58 +++++++++++++++++++++++++++++++++++++++++++------------ 2 files changed, 98 insertions(+), 12 deletions(-) diff --git a/test/test_core.py b/test/test_core.py index bc2828c..5feb9f1 100644 --- a/test/test_core.py +++ b/test/test_core.py @@ -14,6 +14,15 @@ fixture = { } +complex_fixture = { + 'name': 'Mixmaster', + 'properties': { + 'sub': {'type': 'object', + 'properties': {'foo': {'type': 'string'}}} + }, +} + + class TestCore(unittest.TestCase): def test_create_invalid_object(self): Country = warlock.model_factory(fixture) @@ -63,6 +72,49 @@ class TestCore(unittest.TestCase): self.assertEqual(set(sweden.items()), set([('name', 'Sweden'), ('population', 9379116)])) + def test_update(self): + Country = warlock.model_factory(fixture) + sweden = Country(name='Sweden', population=9379116) + exc = warlock.InvalidOperation + self.assertRaises(exc, sweden.update, {'population': 'N/A'}) + self.assertRaises(exc, sweden.update, {'overloard': 'Bears'}) + + def test_deepcopy(self): + """Make sure we aren't leaking references.""" + Mixmaster = warlock.model_factory(complex_fixture) + mike = Mixmaster(sub={'foo': 'mike'}) + + self.assertEquals(mike.sub['foo'], 'mike') + + mike_1 = mike.copy() + mike_1['sub']['foo'] = 'james' + self.assertEquals(mike.sub['foo'], 'mike') + + mike_2 = dict(mike.iteritems()) + mike_2['sub']['foo'] = 'james' + self.assertEquals(mike.sub['foo'], 'mike') + + mike_2 = dict(mike.items()) + mike_2['sub']['foo'] = 'james' + self.assertEquals(mike.sub['foo'], 'mike') + + mike_3_sub = list(mike.itervalues())[0] + mike_3_sub['foo'] = 'james' + self.assertEquals(mike.sub['foo'], 'mike') + + mike_3_sub = list(mike.values())[0] + mike_3_sub['foo'] = 'james' + self.assertEquals(mike.sub['foo'], 'mike') + + def test_forbidden_methods(self): + Country = warlock.model_factory(fixture) + sweden = Country(name='Sweden', population=9379116) + exc = warlock.InvalidOperation + self.assertRaises(exc, sweden.clear) + self.assertRaises(exc, sweden.pop, 0) + self.assertRaises(exc, sweden.popitem) + self.assertRaises(exc, sweden.__delitem__, 'name') + def test_dict_syntax(self): Country = warlock.model_factory(fixture) sweden = Country(name='Sweden', population=9379116) diff --git a/warlock/core.py b/warlock/core.py index eacd7ab..ad3f3bd 100644 --- a/warlock/core.py +++ b/warlock/core.py @@ -27,16 +27,20 @@ def model_factory(schema): except jsonschema.ValidationError: raise ValidationError() - class Model(object): + class Model(dict): """Self-validating model for arbitrary objects""" - def __init__(self, **kwargs): + + def __init__(self, *args, **kwargs): + d = dict(*args, **kwargs) + + # we overload setattr so set this manually self.__dict__['validator'] = validator try: - self.__dict__['validator'](kwargs) + self.validator(d) except ValidationError: raise ValueError() else: - self.__dict__['raw'] = kwargs + dict.__init__(self, d) self.__dict__['changes'] = {} @@ -46,27 +50,57 @@ def model_factory(schema): except KeyError: raise AttributeError(key) - def __getitem__(self, key): - return self.__dict__['raw'][key] - def __setitem__(self, key, value): - mutation = copy.deepcopy(self.__dict__['raw']) + mutation = dict(self.items()) mutation[key] = value try: - self.__dict__['validator'](mutation) + self.validator(mutation) except ValidationError: raise InvalidOperation() - self.__dict__['raw'] = mutation + + dict.__setitem__(self, key, value) + self.__dict__['changes'][key] = value def __setattr__(self, key, value): self.__setitem__(key, value) + def clear(self): + raise InvalidOperation() + + def pop(self, key, default=None): + raise InvalidOperation() + + def popitem(self): + raise InvalidOperation() + + def __delitem__(self, key): + raise InvalidOperation() + + # NOTE(termie): This is kind of the opposite of what copy usually does + def copy(self): + return copy.deepcopy(dict(self)) + + def update(self, other): + mutation = dict(self.items()) + mutation.update(other) + try: + self.validator(mutation) + except ValidationError: + raise InvalidOperation() + dict.update(self, other) + def iteritems(self): - return copy.deepcopy(self.__dict__['raw']).iteritems() + return copy.deepcopy(dict(self)).iteritems() def items(self): - return copy.deepcopy(self.__dict__['raw']).items() + return copy.deepcopy(dict(self)).items() + + def itervalues(self): + return copy.deepcopy(dict(self)).itervalues() + + def values(self): + return copy.deepcopy(dict(self)).values() @property def changes(self): -- cgit v1.2.1