From 536bf8d1d8630c467be5c651b0c790a65c31353c Mon Sep 17 00:00:00 2001 From: Noah Levitt Date: Tue, 28 Feb 2017 17:56:44 -0800 Subject: [PATCH] implement or explicitly disallow other top-level dict operations on ORM Document, and add more automated tests --- doublethink/orm.py | 42 +++++++++--------- setup.py | 2 +- tests/test_rethinker.py | 94 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 111 insertions(+), 27 deletions(-) diff --git a/doublethink/orm.py b/doublethink/orm.py index a9aaf0a..c264f2f 100644 --- a/doublethink/orm.py +++ b/doublethink/orm.py @@ -47,7 +47,7 @@ class WatchedDict(dict, object): def popitem(self): self.callback(self.field) - return dict.popitem() + return dict.popitem(self) def setdefault(self, *args): self.callback(self.field) @@ -57,9 +57,8 @@ class WatchedDict(dict, object): else: return dict.setdefault(self, *args) - def update(self, *args, **kwargs): - # looks a little tricky - raise Exception('not implemented') + # XXX worth implementing? + update = None class WatchedList(list, object): def __init__(self, l, callback, field): @@ -100,9 +99,8 @@ class WatchedList(list, object): self.callback(self.field) return list.pop(self, index) - def clear(self): - self.callback(self.field) - return list.clear(self) + # python 2.7 doesn't have this anyway + clear = None def sort(self, key=None, reverse=False): self.callback(self.field) @@ -157,14 +155,6 @@ class Document(dict, object): @classproperty def table(cls): - ''' - Returns default table name, which is the class name, lowercased. - - Subclasses can override the table name like so: - - class Something(doublethink.Document): - table = 'my_table_name' - ''' return cls.__name__.lower() @classmethod @@ -214,7 +204,20 @@ class Document(dict, object): if key in self._updates: del self._updates[key] - # XXX probably need the other stuff like in WatchedDict + def setdefault(self, *args): + need_update = False + if not args[0] in self: + need_update = True + result = dict.setdefault(self, *args) + if need_update: + self._updated(args[0]) + return result + + # dict methods we don't want to support + clear = None + pop = None + popitem = None + update = None def _updated(self, field): # callback for all updates @@ -277,11 +280,12 @@ class Document(dict, object): 'unexpected result %s from rethinkdb query %s' % ( result, query)) if not should_insert and self._deletes: - self._r.table(self.table).replace( - r.row.without(self._deletes)).run() + query = self._r.table(self.table).replace( + r.row.without(self._deletes)) + result = query.run() if result['errors']: # primary key not found should_insert = True - elif not result['replaced'] == 0: + elif result['replaced'] != 1: raise Exception( 'unexpected result %s from rethinkdb query %s' % ( result, query)) diff --git a/setup.py b/setup.py index 370a5ce..ab1d077 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import codecs setuptools.setup( name='doublethink', - version='0.2.0.dev64', + version='0.2.0.dev65', packages=['doublethink'], classifiers=[ 'Programming Language :: Python :: 2.7', diff --git a/tests/test_rethinker.py b/tests/test_rethinker.py index 1385478..5ba6295 100644 --- a/tests/test_rethinker.py +++ b/tests/test_rethinker.py @@ -321,16 +321,14 @@ def test_orm(r): assert d._updates == { 'm': 'n', 'c': {'d': 'e', 'o': 'p'}, 'f': ['q', 'h'], 'i': 't'} + # list manipulations d.f.append(['sublist']) assert d._updates == { 'm': 'n', 'c': {'d': 'e', 'o': 'p'}, 'f': ['q', 'h', ['sublist']], 'i': 't'} - ### list.clear not in python 2.7 - # d.f[2].clear() - # assert d._updates == { - # 'm': 'n', 'c': {'d': 'e', 'o': 'p'}, - # 'f': ['q', 'h', []], 'i': 't'} + with pytest.raises(TypeError): + d.f[2].clear() result = d.f.pop() assert result == ['sublist'] @@ -364,20 +362,102 @@ def test_orm(r): 'm': 'n', 'c': {'d': 'e', 'o': 'p'}, 'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'} - expected = dict(d) + # more nested field dict operations + del d['c']['d'] + assert d._updates == { + 'm': 'n', 'c': {'o': 'p'}, + 'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'} + + d['c'].clear() + assert d._updates == { + 'm': 'n', 'c': {}, + 'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'} + + assert d['c'].setdefault('aa') is None + assert d._updates == { + 'm': 'n', 'c': {'aa': None}, + 'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'} + + d['c'].setdefault('aa', 'bb') is None + assert d._updates == { + 'm': 'n', 'c': {'aa': None}, + 'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'} + + d['c'].setdefault('cc', 'dd') == 'dd' + assert d._updates == { + 'm': 'n', 'c': {'aa': None, 'cc': 'dd'}, + 'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'} + + d['c'].setdefault('cc') == 'dd' + assert d._updates == { + 'm': 'n', 'c': {'aa': None, 'cc': 'dd'}, + 'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'} + + d['c'].setdefault('cc', 'ee') == 'dd' + assert d._updates == { + 'm': 'n', 'c': {'aa': None, 'cc': 'dd'}, + 'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'} + + assert d['c'].pop('cc') == 'dd' + assert d._updates == { + 'm': 'n', 'c': {'aa': None}, + 'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'} + + assert d['f'][2].popitem() + assert d._updates['f'][2] in ({'w':'x'}, {'y':'z'}) + d.save() assert d._updates == {} assert d._deletes == set() d_copy = SomeDoc.load(r, d.id) assert d == d_copy - d['zuh'] = 'toot' d.save() assert d != d_copy d_copy.refresh() assert d == d_copy + # top level dict operations + with pytest.raises(TypeError): + d.clear() + + with pytest.raises(TypeError): + d.pop('m') + + with pytest.raises(TypeError): + d.popitem() + + with pytest.raises(TypeError): + d.update({'x':'y'}) + + assert d.setdefault('ee') is None + assert d._updates == {'ee': None} + + d.setdefault('ee', 'ff') is None + assert d._updates == {'ee': None} + + d.setdefault('gg', 'hh') == 'hh' + assert d._updates == {'ee': None, 'gg': 'hh'} + + d.setdefault('gg') == 'hh' + assert d._updates == {'ee': None, 'gg': 'hh'} + + d.setdefault('gg', 'ii') == 'hh' + assert d._updates == {'ee': None, 'gg': 'hh'} + + d.save() + assert d._updates == {} + assert d._deletes == set() + + d_copy = SomeDoc.load(r, d.id) + assert d == d_copy + d['yuh'] = 'soot' + d.save() + assert d != d_copy + d_copy.refresh() + assert d == d_copy + def test_orm_pk(r): class NonstandardPrimaryKey(doublethink.Document): @classmethod