def _get_changed_fields(self): """Return a list of all fields that have explicitly been changed. """ EmbeddedDocument = _import_class('EmbeddedDocument') SortedListField = _import_class('SortedListField') changed_fields = [] changed_fields += getattr(self, '_changed_fields', []) for field_name in self._fields_ordered: db_field_name = self._db_field_map.get(field_name, field_name) key = '%s.' % db_field_name data = self._data.get(field_name, None) field = self._fields.get(field_name) if db_field_name in changed_fields: # Whole field already marked as changed, no need to go further continue if isinstance(data, EmbeddedDocument): # Find all embedded fields that have been changed changed = data._get_changed_fields() changed_fields += ['%s%s' % (key, k) for k in changed if k] elif isinstance(data, (list, tuple, dict)): if isinstance(field, SortedListField) and field._ordering: # if ordering is affected whole list is changed if any(field._ordering in d._changed_fields for d in data): changed_fields.append(db_field_name) continue self._nestable_types_changed_fields(changed_fields, key, data) return changed_fields
def to_mongo(self, value, fields=None): """Convert a Python type to a MongoDB-compatible type.""" Document = _import_class('Document') EmbeddedDocument = _import_class('EmbeddedDocument') if isinstance(value, six.string_types): return value if hasattr(value, 'to_mongo'): cls = value.__class__ val = value.to_mongo(fields) # If it's a document that is not inherited add _cls if isinstance(value, EmbeddedDocument): val['_cls'] = cls.__name__ return val is_list = False if not hasattr(value, 'items'): try: is_list = True value = {k: v for k, v in enumerate(value)} except TypeError: # Not iterable return the value return value if self.field: value_dict = { key: self.field._to_mongo_safe_call(item, fields) for key, item in iteritems(value) } else: value_dict = {} for k, v in iteritems(value): if hasattr(v, 'to_mongo'): cls = v.__class__ val = v.to_mongo(fields) # If it's a document that is not inherited add _cls if isinstance(v, (Document, EmbeddedDocument)): val['_cls'] = cls.__name__ value_dict[k] = val else: value_dict[k] = self.to_mongo(v, fields) if is_list: # Convert back to a list return [ v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0)) ] return value_dict
def __init__(self, dict_items, instance, name): BaseDocument = _import_class('BaseDocument') if isinstance(instance, BaseDocument): self._instance = weakref.proxy(instance) self._name = name super(BaseDict, self).__init__(dict_items)
def __set__(self, instance, value): """Descriptor for assigning a value to a field in a document. """ # If setting to None and there is a default # Then set the value to the default value if value is None: if self.null: value = None elif self.default is not None: value = self.default if callable(value): value = value() if instance._initialised: try: if (self.name not in instance._data or instance._data[self.name] != value): instance._mark_as_changed(self.name) except Exception: # Values cant be compared eg: naive and tz datetimes # So mark it as changed instance._mark_as_changed(self.name) EmbeddedDocument = _import_class('EmbeddedDocument') if isinstance(value, EmbeddedDocument): value._instance = weakref.proxy(instance) elif isinstance(value, (list, tuple)): for v in value: if isinstance(v, EmbeddedDocument): v._instance = weakref.proxy(instance) instance._data[self.name] = value
def _validate_choices(self, value): Document = _import_class('Document') EmbeddedDocument = _import_class('EmbeddedDocument') choice_list = self.choices if isinstance(next(iter(choice_list)), (list, tuple)): # next(iter) is useful for sets choice_list = [k for k, _ in choice_list] # Choices which are other types of Documents if isinstance(value, (Document, EmbeddedDocument)): if not any(isinstance(value, c) for c in choice_list): self.error('Value must be an instance of %s' % (six.text_type(choice_list))) # Choices which are types other than Documents else: values = value if isinstance(value, (list, tuple)) else [value] if len(set(values) - set(choice_list)): self.error('Value must be one of %s' % six.text_type(choice_list))
def __getitem__(self, key): value = super(BaseDict, self).__getitem__(key) EmbeddedDocument = _import_class('EmbeddedDocument') if isinstance(value, EmbeddedDocument) and value._instance is None: value._instance = self._instance elif isinstance(value, dict) and not isinstance(value, BaseDict): value = BaseDict(value, None, '%s.%s' % (self._name, key)) super(BaseDict, self).__setitem__(key, value) value._instance = self._instance elif isinstance(value, list) and not isinstance(value, BaseList): value = BaseList(value, None, '%s.%s' % (self._name, key)) super(BaseDict, self).__setitem__(key, value) value._instance = self._instance return value
def __get__(self, instance, owner): if instance is None: # Document class being used rather than a document object return self EmbeddedDocumentListField = _import_class('EmbeddedDocumentListField') value = super(ComplexBaseField, self).__get__(instance, owner) # Convert lists / values so we can watch for any changes on them if isinstance(value, (list, tuple)): if (issubclass(type(self), EmbeddedDocumentListField) and not isinstance(value, EmbeddedDocumentList)): value = EmbeddedDocumentList(value, instance, self.name) elif not isinstance(value, BaseList): value = BaseList(value, instance, self.name) instance._data[self.name] = value elif isinstance(value, dict) and not isinstance(value, BaseDict): value = BaseDict(value, instance, self.name) instance._data[self.name] = value return value
def validate(self, clean=True): """Ensure that all fields' values are valid and that required fields are present. """ # Ensure that each field is matched to a valid value errors = {} if clean: try: self.clean() except ValidationError as error: errors[NON_FIELD_ERRORS] = error # Get a list of tuples of field names and their current values fields = [(self._fields.get(name), self._data.get(name)) for name in self._fields_ordered] EmbeddedDocumentField = _import_class('EmbeddedDocumentField') for field, value in fields: if value is not None: try: if isinstance(field, EmbeddedDocumentField): field._validate(value, clean=clean) else: field._validate(value) except ValidationError as error: errors[field.name] = error.errors or error except (ValueError, AttributeError, AssertionError) as error: errors[field.name] = error elif field.required and not getattr(field, '_auto_gen', False): errors[field.name] = ValidationError('Field is required', field_name=field.name) if errors: pk = 'None' if hasattr(self, 'pk'): pk = self.pk elif self._instance and hasattr(self._instance, 'pk'): pk = self._instance.pk message = 'ValidationError (%s:%s) ' % (self._class_name, pk) raise ValidationError(message, errors=errors)
def to_python(self, value): """Convert a MongoDB-compatible type to a Python type.""" if isinstance(value, six.string_types): return value if hasattr(value, 'to_python'): return value.to_python() BaseDocument = _import_class('BaseDocument') if isinstance(value, BaseDocument): # Something is wrong, return the value as it is return value is_list = False if not hasattr(value, 'items'): try: is_list = True value = {idx: v for idx, v in enumerate(value)} except TypeError: # Not iterable return the value return value if self.field: value_dict = { key: self.field.to_python(item) for key, item in value.items() } else: value_dict = {} for k, v in value.items(): if hasattr(v, 'to_python'): value_dict[k] = v.to_python() else: value_dict[k] = self.to_python(v) if is_list: # Convert back to a list return [ v for _, v in sorted(value_dict.items(), key=operator.itemgetter(0)) ] return value_dict
def __getitem__(self, key): value = super(BaseList, self).__getitem__(key) if isinstance(key, slice): # When receiving a slice operator, we don't convert the structure and bind # to parent's instance. This is buggy for now but would require more work to be handled properly return value EmbeddedDocument = _import_class('EmbeddedDocument') if isinstance(value, EmbeddedDocument) and value._instance is None: value._instance = self._instance elif isinstance(value, dict) and not isinstance(value, BaseDict): # Replace dict by BaseDict value = BaseDict(value, None, '%s.%s' % (self._name, key)) super(BaseList, self).__setitem__(key, value) value._instance = self._instance elif isinstance(value, list) and not isinstance(value, BaseList): # Replace list by BaseList value = BaseList(value, None, '%s.%s' % (self._name, key)) super(BaseList, self).__setitem__(key, value) value._instance = self._instance return value
def _import_classes(mcs): Document = _import_class('Document') EmbeddedDocument = _import_class('EmbeddedDocument') DictField = _import_class('DictField') return Document, EmbeddedDocument, DictField
def __new__(mcs, name, bases, attrs): flattened_bases = mcs._get_bases(bases) super_new = super(DocumentMetaclass, mcs).__new__ # If a base class just call super metaclass = attrs.get('my_metaclass') if metaclass and issubclass(metaclass, DocumentMetaclass): return super_new(mcs, name, bases, attrs) attrs['_is_document'] = attrs.get('_is_document', False) # EmbeddedDocuments could have meta data for inheritance if 'meta' in attrs: attrs['_meta'] = attrs.pop('meta') # EmbeddedDocuments should inherit meta data if '_meta' not in attrs: meta = MetaDict() for base in flattened_bases[::-1]: # Add any mixin metadata from plain objects if hasattr(base, 'meta'): meta.merge(base.meta) elif hasattr(base, '_meta'): meta.merge(base._meta) attrs['_meta'] = meta # 789: EmbeddedDocument shouldn't inherit abstract attrs['_meta']['abstract'] = False # If allow_inheritance is True, add a "_cls" string field to the attrs if attrs['_meta'].get('allow_inheritance'): StringField = _import_class('StringField') attrs['_cls'] = StringField() # Handle document Fields # Merge all fields from subclasses doc_fields = {} for base in flattened_bases[::-1]: if hasattr(base, '_fields'): doc_fields.update(base._fields) # Standard object mixin - merge in any Fields if not hasattr(base, '_meta'): base_fields = {} for attr_name, attr_value in iteritems(base.__dict__): if not isinstance(attr_value, BaseField): continue attr_value.name = attr_name if not attr_value.db_field: attr_value.db_field = attr_name base_fields[attr_name] = attr_value doc_fields.update(base_fields) # Discover any document fields field_names = {} for attr_name, attr_value in iteritems(attrs): if not isinstance(attr_value, BaseField): continue attr_value.name = attr_name if not attr_value.db_field: attr_value.db_field = attr_name doc_fields[attr_name] = attr_value # Count names to ensure no db_field redefinitions field_names[attr_value.db_field] = field_names.get( attr_value.db_field, 0) + 1 # Ensure no duplicate db_fields duplicate_db_fields = [k for k, v in field_names.items() if v > 1] if duplicate_db_fields: msg = ('Multiple db_fields defined for: %s ' % ', '.join(duplicate_db_fields)) raise InvalidDocumentError(msg) # Set _fields and db_field maps attrs['_fields'] = doc_fields attrs['_db_field_map'] = { k: getattr(v, 'db_field', k) for k, v in doc_fields.items() } attrs['_reverse_db_field_map'] = { v: k for k, v in attrs['_db_field_map'].items() } attrs['_fields_ordered'] = tuple(i[1] for i in sorted( (v.creation_counter, v.name) for v in itervalues(doc_fields))) # # Set document hierarchy # superclasses = () class_name = [name] for base in flattened_bases: if (not getattr(base, '_is_base_cls', True) and not getattr(base, '_meta', {}).get('abstract', True)): # Collate hierarchy for _cls and _subclasses class_name.append(base.__name__) if hasattr(base, '_meta'): # Warn if allow_inheritance isn't set and prevent # inheritance of classes where inheritance is set to False allow_inheritance = base._meta.get('allow_inheritance') if not allow_inheritance and not base._meta.get('abstract'): raise ValueError( 'Document %s may not be subclassed. ' 'To enable inheritance, use the "allow_inheritance" meta attribute.' % base.__name__) # Get superclasses from last base superclass document_bases = [ b for b in flattened_bases if hasattr(b, '_class_name') ] if document_bases: superclasses = document_bases[0]._superclasses superclasses += (document_bases[0]._class_name, ) _cls = '.'.join(reversed(class_name)) attrs['_class_name'] = _cls attrs['_superclasses'] = superclasses attrs['_subclasses'] = (_cls, ) attrs['_types'] = attrs['_subclasses'] # TODO depreciate _types # Create the new_class new_class = super_new(mcs, name, bases, attrs) # Set _subclasses for base in document_bases: if _cls not in base._subclasses: base._subclasses += (_cls, ) base._types = base._subclasses # TODO depreciate _types (Document, EmbeddedDocument, DictField) = mcs._import_classes() if issubclass(new_class, Document): new_class._collection = None # Add class to the _document_registry _document_registry[new_class._class_name] = new_class # In Python 2, User-defined methods objects have special read-only # attributes 'im_func' and 'im_self' which contain the function obj # and class instance object respectively. With Python 3 these special # attributes have been replaced by __func__ and __self__. The Blinker # module continues to use im_func and im_self, so the code below # copies __func__ into im_func and __self__ into im_self for # classmethod objects in Document derived classes. if six.PY3: for val in new_class.__dict__.values(): if isinstance(val, classmethod): f = val.__get__(new_class) if hasattr(f, '__func__') and not hasattr(f, 'im_func'): f.__dict__.update({'im_func': getattr(f, '__func__')}) if hasattr(f, '__self__') and not hasattr(f, 'im_self'): f.__dict__.update({'im_self': getattr(f, '__self__')}) return new_class