metaclasses.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. import warnings
  2. import six
  3. from mongoengine.base.common import _document_registry
  4. from mongoengine.base.fields import BaseField, ComplexBaseField, ObjectIdField
  5. from mongoengine.common import _import_class
  6. from mongoengine.errors import InvalidDocumentError
  7. from mongoengine.queryset import (DO_NOTHING, DoesNotExist,
  8. MultipleObjectsReturned,
  9. QuerySetManager)
  10. __all__ = ('DocumentMetaclass', 'TopLevelDocumentMetaclass')
  11. class DocumentMetaclass(type):
  12. """Metaclass for all documents."""
  13. # TODO lower complexity of this method
  14. def __new__(mcs, name, bases, attrs):
  15. flattened_bases = mcs._get_bases(bases)
  16. super_new = super(DocumentMetaclass, mcs).__new__
  17. # If a base class just call super
  18. metaclass = attrs.get('my_metaclass')
  19. if metaclass and issubclass(metaclass, DocumentMetaclass):
  20. return super_new(mcs, name, bases, attrs)
  21. attrs['_is_document'] = attrs.get('_is_document', False)
  22. attrs['_cached_reference_fields'] = []
  23. # EmbeddedDocuments could have meta data for inheritance
  24. if 'meta' in attrs:
  25. attrs['_meta'] = attrs.pop('meta')
  26. # EmbeddedDocuments should inherit meta data
  27. if '_meta' not in attrs:
  28. meta = MetaDict()
  29. for base in flattened_bases[::-1]:
  30. # Add any mixin metadata from plain objects
  31. if hasattr(base, 'meta'):
  32. meta.merge(base.meta)
  33. elif hasattr(base, '_meta'):
  34. meta.merge(base._meta)
  35. attrs['_meta'] = meta
  36. attrs['_meta']['abstract'] = False # 789: EmbeddedDocument shouldn't inherit abstract
  37. # If allow_inheritance is True, add a "_cls" string field to the attrs
  38. if attrs['_meta'].get('allow_inheritance'):
  39. StringField = _import_class('StringField')
  40. attrs['_cls'] = StringField()
  41. # Handle document Fields
  42. # Merge all fields from subclasses
  43. doc_fields = {}
  44. for base in flattened_bases[::-1]:
  45. if hasattr(base, '_fields'):
  46. doc_fields.update(base._fields)
  47. # Standard object mixin - merge in any Fields
  48. if not hasattr(base, '_meta'):
  49. base_fields = {}
  50. for attr_name, attr_value in base.__dict__.iteritems():
  51. if not isinstance(attr_value, BaseField):
  52. continue
  53. attr_value.name = attr_name
  54. if not attr_value.db_field:
  55. attr_value.db_field = attr_name
  56. base_fields[attr_name] = attr_value
  57. doc_fields.update(base_fields)
  58. # Discover any document fields
  59. field_names = {}
  60. for attr_name, attr_value in attrs.iteritems():
  61. if not isinstance(attr_value, BaseField):
  62. continue
  63. attr_value.name = attr_name
  64. if not attr_value.db_field:
  65. attr_value.db_field = attr_name
  66. doc_fields[attr_name] = attr_value
  67. # Count names to ensure no db_field redefinitions
  68. field_names[attr_value.db_field] = field_names.get(
  69. attr_value.db_field, 0) + 1
  70. # Ensure no duplicate db_fields
  71. duplicate_db_fields = [k for k, v in field_names.items() if v > 1]
  72. if duplicate_db_fields:
  73. msg = ('Multiple db_fields defined for: %s ' %
  74. ', '.join(duplicate_db_fields))
  75. raise InvalidDocumentError(msg)
  76. # Set _fields and db_field maps
  77. attrs['_fields'] = doc_fields
  78. attrs['_db_field_map'] = {k: getattr(v, 'db_field', k)
  79. for k, v in doc_fields.items()}
  80. attrs['_reverse_db_field_map'] = {
  81. v: k for k, v in attrs['_db_field_map'].items()
  82. }
  83. attrs['_fields_ordered'] = tuple(i[1] for i in sorted(
  84. (v.creation_counter, v.name)
  85. for v in doc_fields.itervalues()))
  86. #
  87. # Set document hierarchy
  88. #
  89. superclasses = ()
  90. class_name = [name]
  91. for base in flattened_bases:
  92. if (not getattr(base, '_is_base_cls', True) and
  93. not getattr(base, '_meta', {}).get('abstract', True)):
  94. # Collate hierarchy for _cls and _subclasses
  95. class_name.append(base.__name__)
  96. if hasattr(base, '_meta'):
  97. # Warn if allow_inheritance isn't set and prevent
  98. # inheritance of classes where inheritance is set to False
  99. allow_inheritance = base._meta.get('allow_inheritance')
  100. if not allow_inheritance and not base._meta.get('abstract'):
  101. raise ValueError('Document %s may not be subclassed. '
  102. 'To enable inheritance, use the "allow_inheritance" meta attribute.' %
  103. base.__name__)
  104. # Get superclasses from last base superclass
  105. document_bases = [b for b in flattened_bases
  106. if hasattr(b, '_class_name')]
  107. if document_bases:
  108. superclasses = document_bases[0]._superclasses
  109. superclasses += (document_bases[0]._class_name, )
  110. _cls = '.'.join(reversed(class_name))
  111. attrs['_class_name'] = _cls
  112. attrs['_superclasses'] = superclasses
  113. attrs['_subclasses'] = (_cls, )
  114. attrs['_types'] = attrs['_subclasses'] # TODO depreciate _types
  115. # Create the new_class
  116. new_class = super_new(mcs, name, bases, attrs)
  117. # Set _subclasses
  118. for base in document_bases:
  119. if _cls not in base._subclasses:
  120. base._subclasses += (_cls,)
  121. base._types = base._subclasses # TODO depreciate _types
  122. (Document, EmbeddedDocument, DictField,
  123. CachedReferenceField) = mcs._import_classes()
  124. if issubclass(new_class, Document):
  125. new_class._collection = None
  126. # Add class to the _document_registry
  127. _document_registry[new_class._class_name] = new_class
  128. # In Python 2, User-defined methods objects have special read-only
  129. # attributes 'im_func' and 'im_self' which contain the function obj
  130. # and class instance object respectively. With Python 3 these special
  131. # attributes have been replaced by __func__ and __self__. The Blinker
  132. # module continues to use im_func and im_self, so the code below
  133. # copies __func__ into im_func and __self__ into im_self for
  134. # classmethod objects in Document derived classes.
  135. if six.PY3:
  136. for val in new_class.__dict__.values():
  137. if isinstance(val, classmethod):
  138. f = val.__get__(new_class)
  139. if hasattr(f, '__func__') and not hasattr(f, 'im_func'):
  140. f.__dict__.update({'im_func': getattr(f, '__func__')})
  141. if hasattr(f, '__self__') and not hasattr(f, 'im_self'):
  142. f.__dict__.update({'im_self': getattr(f, '__self__')})
  143. # Handle delete rules
  144. for field in new_class._fields.itervalues():
  145. f = field
  146. if f.owner_document is None:
  147. f.owner_document = new_class
  148. delete_rule = getattr(f, 'reverse_delete_rule', DO_NOTHING)
  149. if isinstance(f, CachedReferenceField):
  150. if issubclass(new_class, EmbeddedDocument):
  151. raise InvalidDocumentError('CachedReferenceFields is not '
  152. 'allowed in EmbeddedDocuments')
  153. if not f.document_type:
  154. raise InvalidDocumentError(
  155. 'Document is not available to sync')
  156. if f.auto_sync:
  157. f.start_listener()
  158. f.document_type._cached_reference_fields.append(f)
  159. if isinstance(f, ComplexBaseField) and hasattr(f, 'field'):
  160. delete_rule = getattr(f.field,
  161. 'reverse_delete_rule',
  162. DO_NOTHING)
  163. if isinstance(f, DictField) and delete_rule != DO_NOTHING:
  164. msg = ('Reverse delete rules are not supported '
  165. 'for %s (field: %s)' %
  166. (field.__class__.__name__, field.name))
  167. raise InvalidDocumentError(msg)
  168. f = field.field
  169. if delete_rule != DO_NOTHING:
  170. if issubclass(new_class, EmbeddedDocument):
  171. msg = ('Reverse delete rules are not supported for '
  172. 'EmbeddedDocuments (field: %s)' % field.name)
  173. raise InvalidDocumentError(msg)
  174. f.document_type.register_delete_rule(new_class,
  175. field.name, delete_rule)
  176. if (field.name and hasattr(Document, field.name) and
  177. EmbeddedDocument not in new_class.mro()):
  178. msg = ('%s is a document method and not a valid '
  179. 'field name' % field.name)
  180. raise InvalidDocumentError(msg)
  181. return new_class
  182. @classmethod
  183. def _get_bases(mcs, bases):
  184. if isinstance(bases, BasesTuple):
  185. return bases
  186. seen = []
  187. bases = mcs.__get_bases(bases)
  188. unique_bases = (b for b in bases if not (b in seen or seen.append(b)))
  189. return BasesTuple(unique_bases)
  190. @classmethod
  191. def __get_bases(mcs, bases):
  192. for base in bases:
  193. if base is object:
  194. continue
  195. yield base
  196. for child_base in mcs.__get_bases(base.__bases__):
  197. yield child_base
  198. @classmethod
  199. def _import_classes(mcs):
  200. Document = _import_class('Document')
  201. EmbeddedDocument = _import_class('EmbeddedDocument')
  202. DictField = _import_class('DictField')
  203. CachedReferenceField = _import_class('CachedReferenceField')
  204. return Document, EmbeddedDocument, DictField, CachedReferenceField
  205. class TopLevelDocumentMetaclass(DocumentMetaclass):
  206. """Metaclass for top-level documents (i.e. documents that have their own
  207. collection in the database.
  208. """
  209. def __new__(mcs, name, bases, attrs):
  210. flattened_bases = mcs._get_bases(bases)
  211. super_new = super(TopLevelDocumentMetaclass, mcs).__new__
  212. # Set default _meta data if base class, otherwise get user defined meta
  213. if attrs.get('my_metaclass') == TopLevelDocumentMetaclass:
  214. # defaults
  215. attrs['_meta'] = {
  216. 'abstract': True,
  217. 'max_documents': None,
  218. 'max_size': None,
  219. 'ordering': [], # default ordering applied at runtime
  220. 'indexes': [], # indexes to be ensured at runtime
  221. 'id_field': None,
  222. 'index_background': False,
  223. 'index_drop_dups': False,
  224. 'index_opts': None,
  225. 'delete_rules': None,
  226. # allow_inheritance can be True, False, and None. True means
  227. # "allow inheritance", False means "don't allow inheritance",
  228. # None means "do whatever your parent does, or don't allow
  229. # inheritance if you're a top-level class".
  230. 'allow_inheritance': None,
  231. }
  232. attrs['_is_base_cls'] = True
  233. attrs['_meta'].update(attrs.get('meta', {}))
  234. else:
  235. attrs['_meta'] = attrs.get('meta', {})
  236. # Explicitly set abstract to false unless set
  237. attrs['_meta']['abstract'] = attrs['_meta'].get('abstract', False)
  238. attrs['_is_base_cls'] = False
  239. # Set flag marking as document class - as opposed to an object mixin
  240. attrs['_is_document'] = True
  241. # Ensure queryset_class is inherited
  242. if 'objects' in attrs:
  243. manager = attrs['objects']
  244. if hasattr(manager, 'queryset_class'):
  245. attrs['_meta']['queryset_class'] = manager.queryset_class
  246. # Clean up top level meta
  247. if 'meta' in attrs:
  248. del attrs['meta']
  249. # Find the parent document class
  250. parent_doc_cls = [b for b in flattened_bases
  251. if b.__class__ == TopLevelDocumentMetaclass]
  252. parent_doc_cls = None if not parent_doc_cls else parent_doc_cls[0]
  253. # Prevent classes setting collection different to their parents
  254. # If parent wasn't an abstract class
  255. if (parent_doc_cls and 'collection' in attrs.get('_meta', {}) and
  256. not parent_doc_cls._meta.get('abstract', True)):
  257. msg = 'Trying to set a collection on a subclass (%s)' % name
  258. warnings.warn(msg, SyntaxWarning)
  259. del attrs['_meta']['collection']
  260. # Ensure abstract documents have abstract bases
  261. if attrs.get('_is_base_cls') or attrs['_meta'].get('abstract'):
  262. if (parent_doc_cls and
  263. not parent_doc_cls._meta.get('abstract', False)):
  264. msg = 'Abstract document cannot have non-abstract base'
  265. raise ValueError(msg)
  266. return super_new(mcs, name, bases, attrs)
  267. # Merge base class metas.
  268. # Uses a special MetaDict that handles various merging rules
  269. meta = MetaDict()
  270. for base in flattened_bases[::-1]:
  271. # Add any mixin metadata from plain objects
  272. if hasattr(base, 'meta'):
  273. meta.merge(base.meta)
  274. elif hasattr(base, '_meta'):
  275. meta.merge(base._meta)
  276. # Set collection in the meta if its callable
  277. if (getattr(base, '_is_document', False) and
  278. not base._meta.get('abstract')):
  279. collection = meta.get('collection', None)
  280. if callable(collection):
  281. meta['collection'] = collection(base)
  282. meta.merge(attrs.get('_meta', {})) # Top level meta
  283. # Only simple classes (i.e. direct subclasses of Document) may set
  284. # allow_inheritance to False. If the base Document allows inheritance,
  285. # none of its subclasses can override allow_inheritance to False.
  286. simple_class = all([b._meta.get('abstract')
  287. for b in flattened_bases if hasattr(b, '_meta')])
  288. if (
  289. not simple_class and
  290. meta['allow_inheritance'] is False and
  291. not meta['abstract']
  292. ):
  293. raise ValueError('Only direct subclasses of Document may set '
  294. '"allow_inheritance" to False')
  295. # Set default collection name
  296. if 'collection' not in meta:
  297. meta['collection'] = ''.join('_%s' % c if c.isupper() else c
  298. for c in name).strip('_').lower()
  299. attrs['_meta'] = meta
  300. # Call super and get the new class
  301. new_class = super_new(mcs, name, bases, attrs)
  302. meta = new_class._meta
  303. # Set index specifications
  304. meta['index_specs'] = new_class._build_index_specs(meta['indexes'])
  305. # If collection is a callable - call it and set the value
  306. collection = meta.get('collection')
  307. if callable(collection):
  308. new_class._meta['collection'] = collection(new_class)
  309. # Provide a default queryset unless exists or one has been set
  310. if 'objects' not in dir(new_class):
  311. new_class.objects = QuerySetManager()
  312. # Validate the fields and set primary key if needed
  313. for field_name, field in new_class._fields.iteritems():
  314. if field.primary_key:
  315. # Ensure only one primary key is set
  316. current_pk = new_class._meta.get('id_field')
  317. if current_pk and current_pk != field_name:
  318. raise ValueError('Cannot override primary key field')
  319. # Set primary key
  320. if not current_pk:
  321. new_class._meta['id_field'] = field_name
  322. new_class.id = field
  323. # Set primary key if not defined by the document
  324. new_class._auto_id_field = getattr(parent_doc_cls,
  325. '_auto_id_field', False)
  326. if not new_class._meta.get('id_field'):
  327. # After 0.10, find not existing names, instead of overwriting
  328. id_name, id_db_name = mcs.get_auto_id_names(new_class)
  329. new_class._auto_id_field = True
  330. new_class._meta['id_field'] = id_name
  331. new_class._fields[id_name] = ObjectIdField(db_field=id_db_name)
  332. new_class._fields[id_name].name = id_name
  333. new_class.id = new_class._fields[id_name]
  334. new_class._db_field_map[id_name] = id_db_name
  335. new_class._reverse_db_field_map[id_db_name] = id_name
  336. # Prepend id field to _fields_ordered
  337. new_class._fields_ordered = (id_name, ) + new_class._fields_ordered
  338. # Merge in exceptions with parent hierarchy
  339. exceptions_to_merge = (DoesNotExist, MultipleObjectsReturned)
  340. module = attrs.get('__module__')
  341. for exc in exceptions_to_merge:
  342. name = exc.__name__
  343. parents = tuple(getattr(base, name) for base in flattened_bases
  344. if hasattr(base, name)) or (exc,)
  345. # Create new exception and set to new_class
  346. exception = type(name, parents, {'__module__': module})
  347. setattr(new_class, name, exception)
  348. return new_class
  349. @classmethod
  350. def get_auto_id_names(mcs, new_class):
  351. id_name, id_db_name = ('id', '_id')
  352. if id_name not in new_class._fields and \
  353. id_db_name not in (v.db_field for v in new_class._fields.values()):
  354. return id_name, id_db_name
  355. id_basename, id_db_basename, i = 'auto_id', '_auto_id', 0
  356. while id_name in new_class._fields or \
  357. id_db_name in (v.db_field for v in new_class._fields.values()):
  358. id_name = '{0}_{1}'.format(id_basename, i)
  359. id_db_name = '{0}_{1}'.format(id_db_basename, i)
  360. i += 1
  361. return id_name, id_db_name
  362. class MetaDict(dict):
  363. """Custom dictionary for meta classes.
  364. Handles the merging of set indexes
  365. """
  366. _merge_options = ('indexes',)
  367. def merge(self, new_options):
  368. for k, v in new_options.iteritems():
  369. if k in self._merge_options:
  370. self[k] = self.get(k, []) + v
  371. else:
  372. self[k] = v
  373. class BasesTuple(tuple):
  374. """Special class to handle introspection of bases tuple in __new__"""
  375. pass