Source code for json_document.document

# Copyright (C) 2010, 2011 Linaro Limited
#
# Author: Zygmunt Krynicki <zygmunt.krynicki@linaro.org>
#
# This file is part of json-document
#
# json-document is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3
# as published by the Free Software Foundation
#
# json-document is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with json-document.  If not, see <http://www.gnu.org/licenses/>.

"""
json_document.document
----------------------

Document and fragment classes
"""

import copy
from json_schema_validator.errors import SchemaError
from json_schema_validator.schema  import Schema
from json_schema_validator.validator import Validator

from json_document.errors import OrphanedFragmentError


class DefaultValue(object):
    """
    Special default value marker.

    This value is used by DocumentFragment to represent default values, that
    is, values that should be looked up in the DocumentFragment schema's
    default value.
    """


DefaultValue = DefaultValue()


[docs]class DocumentFragment(object): """ Wrapper around a fragment of a document. Fragment may wrap a single item (such as None, bool, int, float, string) or to a container (such as list or dict). You can access the value pointed to with the :attr:`value` property. Each fragment is linked to a :attr:`parent` fragment and the :attr:'`document` itself. When the parent fragment wraps a list or a dictionary then the index (or key) that this fragment was references as is stored in :attr:`item`. Sometimes this linkage becomes broken and a fragment is considered orphaned. Orphaned fragments still allow you to read the value they wrap but since they are no longer associated with any document you cannot set the value anymore. Fragment is also optionally associated with a schema (typically the relevant part of the document schema). When schema is available you can :meth:`validate()` a fragment for correctness. If the schema designates a :attr:`default_value` you can :meth:`revert_to_default()` to discard the current value in favour of the one from the schema. .. note: Fragments cache their children. If you access a fragment item (through the __getitem__ operator) a new fragment instance wrapping that value is created and retained. Fragment cache is only purged if you overwrite the value directly. This also orphans the fragments that were created so far. """ __slots__ = ('_document', '_parent', '_value', '_item', '_schema', '_fragment_cache') def __init__(self, document, parent, value, item=None, schema=None): self._document = document self._parent = parent self._value = value self._item = item self._schema = schema self._fragment_cache = {} @property
[docs] def schema(self): """ Schema associated with this fragment Schema may be None This is a read-only property. Schema is automatically provided when a sub-fragment is accessed on a parent fragment (all the way up to the document). To provide schema for your fragments make sure to include them in the ``properties`` or ``items``. Alternatively you can provide ``additionalProperties`` that will act as a catch-all clause allowing you to define a schema for anything that was not explicitly matched by ``properties``. """ if self._schema is not None: return Schema(self._schema)
@property
[docs] def is_default(self): """ Check if this fragment points to a default value. .. note:: A fragment that points to a value equal to the value of the default is **not** considered default. Only fragments that were not assigned a value previously are considered default. """ return self._value is DefaultValue
[docs] def revert_to_default(self): """ Discard current value and use defaults from the schema. @raises TypeError: when default value does not exist Revert the value that this fragment points to to the default value. """ self._ensure_not_orphaned() if not self.default_value_exists: raise TypeError("Default value does not exist") if self._value is not DefaultValue: # Orphan all existing fragments in our fragment cache for fragment in self._fragment_cache.itervalues(): fragment._orphan() # Purge the fragment cache from this fragment self._fragment_cache = {} # Set the new value self._lowlevel_set_value(DefaultValue) # Bump the document revision self._document._bump_revision()
@property
[docs] def default_value(self): """ Get the default value. Note: This method will raise SchemaError if the default is not defined in the schema """ return self.schema.default
@property
[docs] def default_value_exists(self): """ Returns True if a default value exists for this fragment. The default value can be accessed with :attr:`default_value`. You can also revert the current value to default by calling :meth:`revert_to_default()`. When there is no default value any attempt to use or access it will raise a SchemaError. """ try: self.default_value return True except SchemaError: return False
[docs] def validate(self): """ Validate the fragment value against the schema """ if self._schema is not None: Validator.validate(self.schema, self.value)
def _get_value(self): if self.is_default: return self.default_value else: return self._value def _set_value(self, new_value): self._ensure_not_orphaned() if self._value != new_value: # Ensure there are no defaults around self._ensure_not_default() # Orphan all existing fragments in our fragment cache for fragment in self._fragment_cache.itervalues(): fragment._orphan() # Purge the fragment cache from this fragment self._fragment_cache = {} # Set the new value self._lowlevel_set_value(new_value) # Bump the document revision self._document._bump_revision() value = property(_get_value, _set_value, None, """ Value being wrapped by this document fragment. Getting reads the value (if not :attr:`is_default`) from the document or transparently returns the default values from the schema (if :attr:`default_value_exists`). Setting a value instantiates default values in this or any parent fragment. That is, if the value of this fragment or any of the parent fragments is default (:attr:`is_default` returns True), then the default value is copied and used as the effective value instead. When :attr:`is_default` is True setting any value (including the value of :attr:`default_value`) will overwrite the value so that :attr:`is_default` will return False. If you want to *set the default value* use :meth:`revert_to_default()` explicitly. Setting a value that is different from the current value bumps the revision of the whole document. """) def _lowlevel_set_value(self, new_value): """ Low-level set value. Stuff it assumes is done elsewhere: * Does NOT check for default values * Does NOT check for orphans * Does NOT invalidate fragment cache Stuff that is done here: * Updates parent object container value (if has parent) * Updates _value """ # Set the new value if self._parent is not None and self._item is not None: # We should be our parent's cache assert self is self._parent._fragment_cache[self._item] # Update our parent's container value if new_value is DefaultValue: del self._parent._value[self._item] else: self._parent._value[self._item] = new_value # Set the new value directly self._value = new_value def _ensure_not_default(self): """ This method transparently "un-defaults" this fragment and any parent fragments. The DefaultValue marker will be replaced by a deep copy of the default value from the schema. """ if self._value is DefaultValue: if self._parent is not None: self._parent._ensure_not_default() self._lowlevel_set_value(copy.deepcopy(self.schema.default)) def _ensure_not_orphaned(self): """ Ensure that a this document fragment is not orphaned. """ if self.is_orphaned: raise OrphanedFragmentError(self) def _orphan(self): """ Orphan this document fragment by disassociating it from the parent and the document and copying the value so that it is fully independent from the parent. .. note:: This does method _not_ remove the fragment from the parent's fragment cache. This is handled by _set_value() which calls _orhpan() on sub fragments it knows about. """ self._parent = None self._document = None self._value = copy.deepcopy(self._value) @property
[docs] def is_orphaned(self): """ Check if a fragment is orphaned. Orphaned fragments can occur in this scenario:: >>> doc = Document() >>> doc["foo"] = "value" >>> foo = doc["foo"] >>> doc.value = {} >>> foo.is_orphaned True That is, when the parent fragment value is overwritten. """ return (self._document is None and self._parent is None and self._item is not None)
@property
[docs] def document(self): """ The document object (the topmost parent document fragment) """ return self._document
@property
[docs] def parent(self): """ The parent fragment (if any) The document root (typically a :class:`Document` instance) has no parent. If the parent exist then ``fragment.parent[fragment.item]`` points back to the same value as ``fragment`` but wrapped in a different instance of DocumentFragment. """ return self._parent
@property
[docs] def item(self): """ The index of this fragment in the parent collection. Item is named somewhat misleadingly. It is the name of the index that was used to access this fragment from the parent fragment. Typically this is the dictionary key name or list index. """ return self._item
def _get_schema_for_item(self, item): if self.schema is None: return item_schema = None value = self.value if isinstance(value, dict): # For objects/dictionaries # Try accessing schema for specific property first. try: item_schema = self.schema.properties[item] except KeyError: # If that fails try to use additionalProperties, # unless it is False if self.schema.additionalProperties is not False: item_schema = self.schema.additionalProperties # If that fails then we have no schema, sorry # TODO: Maybe support patternProperties later elif isinstance(value, list): # For arrays with array schemas (one schema item per array item) # try to access the schema for a particular item first if isinstance(self.schema.items, list): try: item_schema = self.schema.items[item] except IndexError: # If that fails fall back to additionalItems (not # implemented in json-schema-validator yet) if self.schema.additionalItems is not False: item_schema = self.schema.additionalItems elif isinstance(self.schema.items, dict): # For arrays with single schema for each array item just use # the schema directly. item_schema = self.schema.items return item_schema def _add_sub_fragment_to_cache(self, item, allow_create, create_value): """ Add a new fragment instance to the this fragment's cache. """ self._ensure_not_orphaned() if not isinstance(self.value, (dict, list)): raise TypeError( "DocumentFragment must point to a dictionary or list") item_schema = self._get_schema_for_item(item) try: # Since we are using self.value instead of self._value we are # using defaults transparently. item_value = self.value[item] except (KeyError, IndexError) as ex: if item_schema is not None and "default" in item_schema: item_value = DefaultValue elif allow_create is True: self._ensure_not_default() self._value[item] = create_value # We need to manually bump the document revision self._document._bump_revision() item_value = create_value else: raise ex if item_schema is not None and "__fragment_cls" in item_schema: fragment_cls = item_schema["__fragment_cls"] else: fragment_cls = DocumentFragment self._fragment_cache[item] = fragment_cls( self._document, self, item_value, item, item_schema) def _get_sub_fragment(self, item, allow_create=False, create_value=None): """ Get a DocumentFragment for the specified item. This method is used to implement __getitem__ and __setitem__. If the item is missing in this fragment and allow_create is True then an appropriate object is constructed. """ if item not in self._fragment_cache: self._add_sub_fragment_to_cache(item, allow_create, create_value) return self._fragment_cache[item] def __getitem__(self, item): """ Get a sub-fragment for the specified item This method will return a DocumentFragment (or DocumentFragment subclass) instance associated with the specified item. """ return self._get_sub_fragment(item) def __setitem__(self, item, new_value): """ Set the value of a sub-fragment. .. note:: unlike :meth:`__getitem__()` this method operates directly on the value. It is equivalent to ``fragment[item].value = new_value`` but it works correctly for missing items. See :attr:`value` for details on how assignment works. """ fragment = self._get_sub_fragment(item, allow_create=True, create_value=new_value) fragment.value = new_value def __contains__(self, item): """ Return True if the specified item is in this fragment. Works as expected for fragments pointing at lists, dictionaries and strings. Raises TypeError for fragments pointing at any other value type. """ return item in self.value def __len__(self): """ Return the length of this fragment's value. Works as expected for fragments pointing at lists, dictionaries and strings. Raises TypeError for fragments pointing at any other value type. """ return len(self.value) def _iter_list(self): for item in range(len(self.value)): yield self[item] def _iter_dict(self): for item in self.value.iterkeys(): yield self[item] def __iter__(self): """ Iterate over the elements of this fragments value. Works as expected for fragments pointing at lists and dictionaries. Raises TypeError for fragments pointing at any other value type. """ if isinstance(self.value, dict): return self._iter_dict() elif isinstance(self.value, list): return self._iter_list() else: raise TypeError("%r is not iterable" % self)
[docs]class Document(DocumentFragment): """ Class representing a smart JSON document A document is also a fragment that wraps the entire value. Is inherits all of its properties. There are two key differences: a document has no parent fragment and it holds the revision counter that is incremented on each modification of the document. """ document_schema = {"type": "any"} __slots__ = DocumentFragment.__slots__ + ('_revision',) def __init__(self, value, schema=None): """ Construct a document with the specified value and schema. Value is required. The schema defaults to document_schema attribute on the class object (which by default it a very simple schema for any objects). """ # Start with an empty object by default # Initialize DocumentFragment super(Document, self).__init__( document=self, parent=None, value=value, item=None, schema=schema or self.__class__.document_schema) # Initially set the revision to 0 self._revision = 0 @property
[docs] def revision(self): """ Return the revision number of this document. Each change increments this value by one. You should not really care about the count as sometimes the increments may be not what you expected. It is best to use this to spot difference (if your count is different than mine we're different). """ return self._revision
def _bump_revision(self): """ Increment the document revision number. This is a private method, it is called by DocumentFragment """ self._revision += 1
[docs]class DocumentPersistence(object): """ Simple glue layer between document and storage:: document <-> serializer <-> storage You can have any number of persistence instances associated with a single document. """ def __init__(self, document, storage, serializer=None): self.document = document self.storage = storage self.serializer = serializer self.last_revision = None
[docs] def load(self): """ Load the document from the storage layer """ text = self.storage.read() obj = self.serializer.loads(text) self.document.value = obj self.last_revision = self.document.revision
[docs] def save(self): """ Save the document to the storage layer. The document is only saved if the document was modified since it was last saved. The document revision is non-persistent property (so you cannot use it as a version control system) but as long as the document instance is alive you can optimize saving easily. """ if self.last_revision != self.document.revision: if self.serializer.needs_real_object: obj = self.document else: obj = self.document.value text = self.serializer.dumps(obj) self.storage.write(text) self.last_revision = self.document.revision
@property def is_dirty(self): return self.last_revision != self.document.revision

Project Versions