Package: document
Document is a simple wrapper for dicts that provides an object-oriented
interface for accessing keys, as well as the ability to add metadata and
utility functions to your data. The primary purpose of theDocumentclass
is to make working with PyMongo data easier, but it is in no way restricted to
this use case. It has no dependencies outside of Python’s standard library.Document is released under MIT license.InstallationDocument can be installed from PyPI:easy_install documentor:pip install documentYou can also simply download thedocumentmodule and add it to your
project.BasicsLet’s first take a look at the constructor. TheDocumentconstructor takes
any number of keyword argument which are stored as a dict internally.>>> from document import Document
>>> my_doc = Document(foo='bar', baz=12)The dictionary keys can be accessed either as properties or keys:>>> my_doc.foo
'bar'
>>> my_doc['baz']
12When using property access, you can also set new keys:>>> my_doc.bar = 1If you access a missing property, you will get aKeyErrorinstead ofAttributeErrorbecause, under the hood, we are looking up dictionary keys
rather than attributes.>>> my_doc.bogus
Traceback (most recent call last):
....
KeyError: 'bogus'This difference is worth noting if you are a practicioner ofEAFP.Unlike normal Python dictionaries, key access can drill down multiple levels.
Consider this example:>>> another_doc = Document(foo={'bar': 'baz'})
>>> another_doc['foo.bar']
'baz'As you can see, using a period in the key name will give us access to the
nested dict’s key. For breviti, we will call such keys ‘multipart’ keys.The multipart keys also work when setting values:>>> another_doc['foo.bar'] = 'fam'
>>> another_doc.foo
{'bar': 'fam'}You can also use theget()method with the multipart keys.>>> another_doc.get('foo.bar')
'fam'
>>> another_doc.get('foo.baz')
NoneTesting for existence of a key works with multipart keys as well:>>> 'foo.bar' in another_doc
True
>>> 'foo.baz' in another_doc
FalseBecause of the multipart keys, you cannot use periods in your keys. Those will
simply become inaccessible through the normal interface. You can still access
them through the private_documentkey, but that is not recommended, since
the private property is an implementation detail and may be renamed or removed
in future releases.AlthoughDocumentsports the full array of dict methods likepop()anditems(), they don’t work with mutlipart keys but only with top-level keys.Apart from dict methods,Documentimplements a few non-standard methods.
One of them isslice()which allows you to get a dict containing a subset
of the keys.>>> a_doc = Document(foo=1, bar=2, baz=3)
>>> a_doc.slice('foo', 'baz')
{'foo': 1, 'baz': 3}To get back the full dict with all keys, use theto_dict()method:>>> a_doc.to_dict()
{'foo': 1, 'bar': 2, 'baz': 3}Note thatto_dict()always returns a copy of the internal dict, not a
reference to it. Any modification you do to the dict returned byto_dict()will not reflect on whatever is stored in the document.For convenience, and for Python purists, theDocumentobject provides afrom_dict()method that returns a new document from a dict.>>> b_doc = Document.from_dict({'foo': 'bar'})
>>> b_doc.foo
'bar'If you don’t care about purity, you can always use the**magic and the
constructor.>>> b_doc = Document(**{'foo': 'bar'})The main difference between usingfrom_dictand the**magic is the
type of the keys that end up in the dict. When you use the magic (and keyword
arguments for that matter), the keys all become strings (in Python 2.x),
whereas unicode keys can be preserved when usingfrom_dict()(and also theupdate()method).ExtendingNow you might be wondering why you need a whole class to deal with dicts when
dicts work perfectly fine in Python. That’s a valid question. The main
motivation behindDocumentwas to allow developers to define custom methods
and especially properties that would be separate from the data, but still
accessible using a similar interface. What this allows us is to have ultitiy
methods and metadata attached to our data, that are not serialized and/or saved
into the database.To demonstrate this we will create a customUserdocument.To create such a document, we first subclass theDocumentclass. This is
generally the intended purpose of theDocumentclass, and you should always
subclass it and add new properties. If you feel you don’t need to subclass, you
can probably get away with a plaindict.Back to our example, let’s say we have a user document that should have anauthenticatedflag that is, for obvious reasons, only used during a
request-response cycle, and not saved to the database. We also want to have a
method that will check passwords, as well as one that will set it. The subclass
might look something like this:class User(Document):
    authenticated = False

    def check_password(self, password):
        return encrypt(password) == self.password

    def set_password(self, password):
        self.password = encrypt(password)Now we can, say, retrieve a dict from a database and convert it to a user
document (using some imaginary database and request API in this example):user_dict = db.users.get(username='foo')
password = request.params['password']
user = User.from_dict(user_dict)
if user.check_password(password):
    user.authenticated = True
    session['user'] = user
    return 'success!'
return 'wrong username or password'Suppose the database expects us to save a new record by passing it a dict
representing the record’s data (which is how PyMongo works, for example).
Let’s store a new user:username = request.params['username']
password = request.params['password']
user = User(usernam=username)
user.set_password(password)
db.users.save(user.to_dict())By using theto_dict()method, we avoid having to deal withauthenticatedproperty, as well as the two methods we have defined on theUserdocument. Only the username and encrypted passwords are saved. This
provides a clean separation of what we consider metadata and actual data.This separation has other consequences. Comparing two records with different
metadata will only compare the actual data. For example:>>> class FooDoc(Document):
...    meta = True
>>> foo1 = FooDoc(foo=1)
>>> foo2 = FooDoc(foo=1)
>>> foo1.meta = False
>>> foo1 == foo2
TrueDespite the two documents having different values for themetaproperty,
they are still considered equal because the actual data is equal.Another thing to note that, because we can have custom properties, and also
assign dictionary keys using properties, only the properties that are defined
on the class can actually be set as properties, and everything else is
considered a dictionary key. To demonstrate this, we will use theFooDocclass defined before.>>> foo1 = FooDoc(foo=1)
>>> foo1.meta = True  # Sets the ``meta`` property
>>> foo1.metadata = 'bar'  # Creates an actual dict key called ``metadata``
>>> foo1.to_dict()
{'foo': 1, 'metadata': 'bar'}API documentationThe wholedocumentmodule is a little under 440 lines of code including
inline documentation and doctests. Therefore, you are advised to look at the
source code for in-depth API documentation. All examples in the inline
documentation double as unit tests so they are virtually guaranteed to work as
documented.Reporting bugsReport all bugs to theBitBucket issue tracker