make ORM __getattr___ return None if attribute is missing; documentation improvements

This commit is contained in:
Noah Levitt 2017-02-28 16:42:57 -08:00
parent ad0e1b1fd7
commit 4a9978fc46
3 changed files with 38 additions and 23 deletions

View File

@ -20,7 +20,7 @@ import rethinkdb as r
import logging import logging
import rethinkstuff import rethinkstuff
class WatchedDict(dict): class WatchedDict(dict, object):
def __init__(self, d, callback, field): def __init__(self, d, callback, field):
self.callback = callback self.callback = callback
self.field = field self.field = field
@ -61,7 +61,7 @@ class WatchedDict(dict):
# looks a little tricky # looks a little tricky
raise Exception('not implemented') raise Exception('not implemented')
class WatchedList(list): class WatchedList(list, object):
def __init__(self, l, callback, field): def __init__(self, l, callback, field):
self.callback = callback self.callback = callback
self.field = field self.field = field
@ -120,7 +120,6 @@ def watch(obj, callback, field):
else: else:
return obj return obj
class classproperty(object): class classproperty(object):
def __init__(self, fget): def __init__(self, fget):
self.fget = fget self.fget = fget
@ -129,10 +128,9 @@ class classproperty(object):
class Document(dict, object): class Document(dict, object):
''' '''
Base class for ORM. Base class for ORM. You should subclass this class for each of your
rethinkdb tables. You can add custom functionality in your subclass if
You should subclass this class for each of your rethinkdb tables. You can appropriate.
add custom functionality in your subclass if appropriate.
This class keeps track of changes made to the object and any nested fields. 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 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 Changes in nested fields result in updates to their first-level ancestor
field. For example, if your document starts as {'a': {'b': 'c'}}, then 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' you run doc['a']['x'] = 'y', then the update will replace the whole 'a'
field. Nested field updates get too complicated any other way. 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 @classproperty
@ -149,7 +160,7 @@ class Document(dict, object):
''' '''
Returns default table name, which is the class name, lowercased. 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): class Something(rethinkstuff.Document):
table = 'my_table_name' table = 'my_table_name'
@ -159,7 +170,7 @@ class Document(dict, object):
@classmethod @classmethod
def load(cls, rethinker, pk): def load(cls, rethinker, pk):
''' '''
Retrieve an instance from the database. Retrieves a document from the database, by primary key.
''' '''
doc = cls(rethinker) doc = cls(rethinker)
doc[doc.pk_field] = pk doc[doc.pk_field] = pk
@ -169,11 +180,8 @@ class Document(dict, object):
@classmethod @classmethod
def table_create(cls, rethinker): def table_create(cls, rethinker):
''' '''
Creates the table. Creates the table. Subclasses may want to override this method to do
more things, such as creating secondary indexes.
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.
''' '''
rethinker.table_create(cls.table).run() rethinker.table_create(cls.table).run()
@ -198,7 +206,7 @@ class Document(dict, object):
self._updated(key) self._updated(key)
__setattr__ = __setitem__ __setattr__ = __setitem__
__getattr__ = dict.__getitem__ __getattr__ = dict.get
def __delitem__(self, key): def __delitem__(self, key):
dict.__delitem__(self, key) dict.__delitem__(self, key)
@ -243,11 +251,18 @@ class Document(dict, object):
def save(self): 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 should_insert = False
try: try:
self.pk_value # raise KeyError if unset self[self.pk_field] # raises KeyError if missing
if self._updates: if self._updates:
# r.literal() to replace, not merge with, nested fields # r.literal() to replace, not merge with, nested fields
updates = {field: r.literal(self._updates[field]) updates = {field: r.literal(self._updates[field])
@ -288,7 +303,7 @@ class Document(dict, object):
def refresh(self): def refresh(self):
''' '''
Refresh from the database. Refresh the document from the database.
''' '''
d = self._r.table(self.table).get(self.pk_value).run() d = self._r.table(self.table).get(self.pk_value).run()
if d is None: if d is None:

View File

@ -3,7 +3,7 @@ import codecs
setuptools.setup( setuptools.setup(
name='rethinkstuff', name='rethinkstuff',
version='0.2.0.dev62', version='0.2.0.dev63',
packages=['rethinkstuff'], packages=['rethinkstuff'],
classifiers=[ classifiers=[
'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 2.7',

View File

@ -411,10 +411,10 @@ def test_orm_pk(r):
# new doc with something in it # new doc with something in it
e = NonstandardPrimaryKey(r, {'some_field': 'something'}) e = NonstandardPrimaryKey(r, {'some_field': 'something'})
with pytest.raises(KeyError):
e.not_id
with pytest.raises(KeyError): with pytest.raises(KeyError):
e['not_id'] e['not_id']
assert e.not_id is None
assert e.get('not_id') is None
e.save() e.save()
assert e.not_id assert e.not_id