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 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:

View File

@ -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',

View File

@ -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