diff --git a/rethinkstuff/orm.py b/rethinkstuff/orm.py index be2cf8f..a9e5d87 100644 --- a/rethinkstuff/orm.py +++ b/rethinkstuff/orm.py @@ -20,7 +20,7 @@ import rethinkdb as r import logging import rethinkstuff -class WatchedDict(dict): +class WatchedDict(dict, object): def __init__(self, d, callback, field): self.callback = callback self.field = field @@ -61,7 +61,7 @@ class WatchedDict(dict): # looks a little tricky raise Exception('not implemented') -class WatchedList(list): +class WatchedList(list, object): def __init__(self, l, callback, field): self.callback = callback self.field = field @@ -120,7 +120,6 @@ def watch(obj, callback, field): else: return obj - class classproperty(object): def __init__(self, fget): self.fget = fget @@ -129,10 +128,9 @@ class classproperty(object): class Document(dict, object): ''' - Base class for ORM. - - You should subclass this class for each of your rethinkdb tables. You can - add custom functionality in your subclass if appropriate. + Base class for ORM. You should subclass this class for each of your + rethinkdb tables. You can add custom functionality in your subclass if + appropriate. This class keeps track of changes made to the object and any nested fields. After you have made some changes, call update() to persist them to the @@ -140,8 +138,21 @@ class Document(dict, object): Changes in nested fields result in updates to their first-level ancestor field. For example, if your document starts as {'a': {'b': 'c'}}, then - you run d['a']['x'] = 'y', then the update will replace the whole 'a' - field. Nested field updates get too complicated any other way. + you run doc['a']['x'] = 'y', then the update will replace the whole 'a' + field. (Nested field updates get too complicated any other way.) + + This class subclasses dict. Thus attributes can be accessed with + `doc['foo']` or `doc.get('foo')`, depending on what you want to happen if + the attribute is missing. In addition, this class overrides `__getattr__` + to point to `dict.get`, so that first level attributes can be accessed as + if they were member variables, e.g. `doc.foo`. If there is no attribute + foo, `doc.foo` returns None. (XXX is this definitely what we want?) + + The default table name is the class name, lowercased. Subclasses can + specify different table name like so: + + class Something(rethinkstuff.Document): + table = 'my_table_name' ''' @classproperty @@ -149,7 +160,7 @@ class Document(dict, object): ''' Returns default table name, which is the class name, lowercased. - Subclasses can override this default more simply: + Subclasses can override the table name like so: class Something(rethinkstuff.Document): table = 'my_table_name' @@ -159,7 +170,7 @@ class Document(dict, object): @classmethod def load(cls, rethinker, pk): ''' - Retrieve an instance from the database. + Retrieves a document from the database, by primary key. ''' doc = cls(rethinker) doc[doc.pk_field] = pk @@ -169,11 +180,8 @@ class Document(dict, object): @classmethod def table_create(cls, rethinker): ''' - Creates the table. - - Can be run on an instance of the class: `my_doc.table_create - Subclasses may want to override this method to do more things, such as - creating indexes. + Creates the table. Subclasses may want to override this method to do + more things, such as creating secondary indexes. ''' rethinker.table_create(cls.table).run() @@ -198,7 +206,7 @@ class Document(dict, object): self._updated(key) __setattr__ = __setitem__ - __getattr__ = dict.__getitem__ + __getattr__ = dict.get def __delitem__(self, key): dict.__delitem__(self, key) @@ -243,11 +251,18 @@ class Document(dict, object): def save(self): ''' - Saves + Persist changes to rethinkdb. Updates only the fields that have + changed. Performs insert rather than update if the document has no + primary key or if the primary key is absent from the database. + + If there have been any changes to nested fields, updates the first + level attribute. For example, if foo['bar']['baz']['quux'] has changed, + all of foo['bar'] is replaced, but foo['something_else'] is not + touched. ''' should_insert = False try: - self.pk_value # raise KeyError if unset + self[self.pk_field] # raises KeyError if missing if self._updates: # r.literal() to replace, not merge with, nested fields updates = {field: r.literal(self._updates[field]) @@ -288,7 +303,7 @@ class Document(dict, object): def refresh(self): ''' - Refresh from the database. + Refresh the document from the database. ''' d = self._r.table(self.table).get(self.pk_value).run() if d is None: diff --git a/setup.py b/setup.py index 0d87437..0942e78 100644 --- a/setup.py +++ b/setup.py @@ -3,7 +3,7 @@ import codecs setuptools.setup( name='rethinkstuff', - version='0.2.0.dev62', + version='0.2.0.dev63', packages=['rethinkstuff'], classifiers=[ 'Programming Language :: Python :: 2.7', diff --git a/tests/test_rethinker.py b/tests/test_rethinker.py index 2a9429a..98b083d 100644 --- a/tests/test_rethinker.py +++ b/tests/test_rethinker.py @@ -411,10 +411,10 @@ def test_orm_pk(r): # new doc with something in it e = NonstandardPrimaryKey(r, {'some_field': 'something'}) - with pytest.raises(KeyError): - e.not_id with pytest.raises(KeyError): e['not_id'] + assert e.not_id is None + assert e.get('not_id') is None e.save() assert e.not_id