diff --git a/mongomap/base.py b/mongomap/base.py new file mode 100644 index 00000000..b6542d8a --- /dev/null +++ b/mongomap/base.py @@ -0,0 +1,101 @@ + +class ValidationError(Exception): + pass + + +class BaseField(object): + """A base class for fields in a MongoDB document. Instances of this class + may be added to subclasses of `Document` to define a document's schema. + """ + + def __init__(self, name=None, default=None): + self.name = name + self.default = default + + def __get__(self, instance, owner): + """Descriptor for retrieving a value from a field in a document. Do + any necessary conversion between Python and MongoDB types. + """ + if instance is None: + # Document class being used rather than a document object + return self + + # Get value from document instance if available, if not use default + value = instance._data.get(self.name) + if value is not None: + value = self._to_python(value) + elif self.default is not None: + value = self.default + if callable(value): + value = value() + return value + + def __set__(self, instance, value): + """Descriptor for assigning a value to a field in a document. Do any + necessary conversion between Python and MongoDB types. + """ + if value is not None: + try: + value = self._to_python(value) + self._validate(value) + value = self._to_mongo(value) + except ValueError: + raise ValidationError('Invalid value for field of type "' + + self.__class__.__name__ + '"') + instance._data[self.name] = value + + def _to_python(self, value): + """Convert a MongoDB-compatible type to a Python type. + """ + return unicode(value) + + def _to_mongo(self, value): + """Convert a Python type to a MongoDB-compatible type. + """ + return self._to_python(value) + + def _validate(self, value): + """Perform validation on a value. + """ + return value + + +class DocumentMetaclass(type): + """Metaclass for all documents. + """ + + def __new__(cls, name, bases, attrs): + doc_fields = {} + + # Include all fields present in superclasses + for base in bases: + if hasattr(base, '_fields'): + doc_fields.update(base._fields) + + # Add the document's fields to the _fields attribute + for attr_name, attr_val in attrs.items(): + if issubclass(attr_val.__class__, BaseField): + if not attr_val.name: + attr_val.name = attr_name + doc_fields[attr_name] = attr_val + attrs['_fields'] = doc_fields + + return type.__new__(cls, name, bases, attrs) + + +class BaseDocument(object): + + def __init__(self, **values): + self._data = {} + # Assign initial values to instance + for attr_name, attr_value in self._fields.items(): + if attr_name in values: + setattr(self, attr_name, values.pop(attr_name)) + else: + # Use default value + setattr(self, attr_name, getattr(self, attr_name)) + + def __iter__(self): + # Use _data rather than _fields as iterator only looks at names so + # values don't need to be converted to Python types + return iter(self._data) diff --git a/mongomap/document.py b/mongomap/document.py index 2fed2be6..96d43003 100644 --- a/mongomap/document.py +++ b/mongomap/document.py @@ -1,29 +1,6 @@ -import pymongo - -import fields - -class DocumentMetaclass(type): - """Metaclass for all documents. - """ - - def __new__(cls, name, bases, attrs): - doc_fields = {} - - # Include all fields present in superclasses - for base in bases: - if hasattr(base, '_fields'): - doc_fields.update(base._fields) - - # Add the document's fields to the _fields attribute - for attr_name, attr_val in attrs.items(): - if issubclass(attr_val.__class__, fields.Field): - if not attr_val.name: - attr_val.name = attr_name - doc_fields[attr_name] = attr_val - attrs['_fields'] = doc_fields - - return type.__new__(cls, name, bases, attrs) +from base import DocumentMetaclass, BaseDocument +#import pymongo class TopLevelDocumentMetaclass(DocumentMetaclass): """Metaclass for top-level documents (i.e. documents that have their own @@ -48,21 +25,12 @@ class TopLevelDocumentMetaclass(DocumentMetaclass): return DocumentMetaclass.__new__(cls, name, bases, attrs) -class Document(object): +class EmbeddedDocument(BaseDocument): + + __metaclass__ = DocumentMetaclass + + +class Document(BaseDocument): __metaclass__ = TopLevelDocumentMetaclass - def __init__(self, **values): - self._data = {} - # Assign initial values to instance - for attr_name, attr_value in self._fields.items(): - if attr_name in values: - setattr(self, attr_name, values.pop(attr_name)) - else: - # Use default value - setattr(self, attr_name, getattr(self, attr_name)) - - def __iter__(self): - # Use _data rather than _fields as iterator only looks at names so - # values don't need to be converted to Python types - return iter(self._data) diff --git a/mongomap/fields.py b/mongomap/fields.py index 928fe5ce..f642f80c 100644 --- a/mongomap/fields.py +++ b/mongomap/fields.py @@ -1,85 +1,20 @@ +from base import BaseField, ValidationError +from document import EmbeddedDocument + import re __all__ = ['StringField', 'IntField', 'ValidationError'] -class ValidationError(Exception): - pass - - -class Field(object): - """A base class for fields in a MongoDB document. Instances of this class - may be added to subclasses of `Document` to define a document's schema. - """ - - def __init__(self, name=None, default=None): - self.name = name - self.default = default - - def __get__(self, instance, owner): - """Descriptor for retrieving a value from a field in a document. Do - any necessary conversion between Python and MongoDB types. - """ - if instance is None: - # Document class being used rather than a document object - return self - - # Get value from document instance if available, if not use default - value = instance._data.get(self.name) - if value is not None: - value = self._to_python(value) - elif self.default is not None: - value = self.default - if callable(value): - value = value() - return value - - def __set__(self, instance, value): - """Descriptor for assigning a value to a field in a document. Do any - necessary conversion between Python and MongoDB types. - """ - if value is not None: - try: - value = self._to_python(value) - self._validate(value) - value = self._to_mongo(value) - except ValueError: - raise ValidationError('Invalid value for field of type "' + - self.__class__.__name__ + '"') - instance._data[self.name] = value - - def _to_python(self, value): - """Convert a MongoDB-compatible type to a Python type. - """ - return unicode(value) - - def _to_mongo(self, value): - """Convert a Python type to a MongoDB-compatible type. - """ - return self._to_python(value) - - def _validate(self, value): - """Perform validation on a value. - """ - return value - - -class NestedDocumentField(Field): - """A nested document field. Only valid values are subclasses of - NestedDocument. - """ - pass - - -class StringField(Field): +class StringField(BaseField): """A unicode string field. """ def __init__(self, regex=None, max_length=None, **kwargs): self.regex = re.compile(regex) if regex else None self.max_length = max_length - Field.__init__(self, **kwargs) + BaseField.__init__(self, **kwargs) def _validate(self, value): if self.max_length is not None and len(value) > self.max_length: @@ -90,13 +25,13 @@ class StringField(Field): raise ValidationError(message) -class IntField(Field): +class IntField(BaseField): """An integer field. """ def __init__(self, min_value=None, max_value=None, **kwargs): self.min_value, self.max_value = min_value, max_value - Field.__init__(self, **kwargs) + BaseField.__init__(self, **kwargs) def _to_python(self, value): return int(value) @@ -107,3 +42,10 @@ class IntField(Field): if self.max_value is not None and value > self.max_value: raise ValidationError('Integer value is too large') + + +class EmbeddedDocumentField(BaseField): + """An embedded document field. Only valid values are subclasses of + EmbeddedDocument. + """ + pass