state.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. from django.apps import AppConfig
  2. from django.apps.registry import Apps
  3. from django.db import models
  4. from django.db.models.options import DEFAULT_NAMES, normalize_together
  5. from django.utils import six
  6. from django.utils.module_loading import import_string
  7. class InvalidBasesError(ValueError):
  8. pass
  9. class ProjectState(object):
  10. """
  11. Represents the entire project's overall state.
  12. This is the item that is passed around - we do it here rather than at the
  13. app level so that cross-app FKs/etc. resolve properly.
  14. """
  15. def __init__(self, models=None):
  16. self.models = models or {}
  17. self.apps = None
  18. def add_model_state(self, model_state):
  19. self.models[(model_state.app_label, model_state.name.lower())] = model_state
  20. def clone(self):
  21. "Returns an exact copy of this ProjectState"
  22. return ProjectState(
  23. models=dict((k, v.clone()) for k, v in self.models.items())
  24. )
  25. def render(self):
  26. "Turns the project state into actual models in a new Apps"
  27. if self.apps is None:
  28. # Populate the app registry with a stub for each application.
  29. app_labels = set(model_state.app_label for model_state in self.models.values())
  30. self.apps = Apps([AppConfigStub(label) for label in sorted(app_labels)])
  31. # We keep trying to render the models in a loop, ignoring invalid
  32. # base errors, until the size of the unrendered models doesn't
  33. # decrease by at least one, meaning there's a base dependency loop/
  34. # missing base.
  35. unrendered_models = list(self.models.values())
  36. while unrendered_models:
  37. new_unrendered_models = []
  38. for model in unrendered_models:
  39. try:
  40. model.render(self.apps)
  41. except InvalidBasesError:
  42. new_unrendered_models.append(model)
  43. if len(new_unrendered_models) == len(unrendered_models):
  44. raise InvalidBasesError("Cannot resolve bases for %r" % new_unrendered_models)
  45. unrendered_models = new_unrendered_models
  46. return self.apps
  47. @classmethod
  48. def from_apps(cls, apps):
  49. "Takes in an Apps and returns a ProjectState matching it"
  50. app_models = {}
  51. for model in apps.get_models():
  52. model_state = ModelState.from_model(model)
  53. app_models[(model_state.app_label, model_state.name.lower())] = model_state
  54. return cls(app_models)
  55. def __eq__(self, other):
  56. if set(self.models.keys()) != set(other.models.keys()):
  57. return False
  58. return all(model == other.models[key] for key, model in self.models.items())
  59. def __ne__(self, other):
  60. return not (self == other)
  61. class AppConfigStub(AppConfig):
  62. """
  63. Stubs a Django AppConfig. Only provides a label, and a dict of models.
  64. """
  65. # Not used, but required by AppConfig.__init__
  66. path = ''
  67. def __init__(self, label):
  68. super(AppConfigStub, self).__init__(label, None)
  69. def import_models(self, all_models):
  70. self.models = all_models
  71. class ModelState(object):
  72. """
  73. Represents a Django Model. We don't use the actual Model class
  74. as it's not designed to have its options changed - instead, we
  75. mutate this one and then render it into a Model as required.
  76. Note that while you are allowed to mutate .fields, you are not allowed
  77. to mutate the Field instances inside there themselves - you must instead
  78. assign new ones, as these are not detached during a clone.
  79. """
  80. def __init__(self, app_label, name, fields, options=None, bases=None):
  81. self.app_label = app_label
  82. self.name = name
  83. self.fields = fields
  84. self.options = options or {}
  85. self.bases = bases or (models.Model, )
  86. # Sanity-check that fields is NOT a dict. It must be ordered.
  87. if isinstance(self.fields, dict):
  88. raise ValueError("ModelState.fields cannot be a dict - it must be a list of 2-tuples.")
  89. @classmethod
  90. def from_model(cls, model):
  91. """
  92. Feed me a model, get a ModelState representing it out.
  93. """
  94. # Deconstruct the fields
  95. fields = []
  96. for field in model._meta.local_fields:
  97. name, path, args, kwargs = field.deconstruct()
  98. field_class = import_string(path)
  99. try:
  100. fields.append((name, field_class(*args, **kwargs)))
  101. except TypeError as e:
  102. raise TypeError("Couldn't reconstruct field %s on %s.%s: %s" % (
  103. name,
  104. model._meta.app_label,
  105. model._meta.object_name,
  106. e,
  107. ))
  108. for field in model._meta.local_many_to_many:
  109. name, path, args, kwargs = field.deconstruct()
  110. field_class = import_string(path)
  111. try:
  112. fields.append((name, field_class(*args, **kwargs)))
  113. except TypeError as e:
  114. raise TypeError("Couldn't reconstruct m2m field %s on %s: %s" % (
  115. name,
  116. model._meta.object_name,
  117. e,
  118. ))
  119. # Extract the options
  120. options = {}
  121. for name in DEFAULT_NAMES:
  122. # Ignore some special options
  123. if name in ["apps", "app_label"]:
  124. continue
  125. elif name in model._meta.original_attrs:
  126. if name == "unique_together":
  127. ut = model._meta.original_attrs["unique_together"]
  128. options[name] = set(normalize_together(ut))
  129. elif name == "index_together":
  130. it = model._meta.original_attrs["index_together"]
  131. options[name] = set(normalize_together(it))
  132. else:
  133. options[name] = model._meta.original_attrs[name]
  134. def flatten_bases(model):
  135. bases = []
  136. for base in model.__bases__:
  137. if hasattr(base, "_meta") and base._meta.abstract:
  138. bases.extend(flatten_bases(base))
  139. else:
  140. bases.append(base)
  141. return bases
  142. # We can't rely on __mro__ directly because we only want to flatten
  143. # abstract models and not the whole tree. However by recursing on
  144. # __bases__ we may end up with duplicates and ordering issues, we
  145. # therefore discard any duplicates and reorder the bases according
  146. # to their index in the MRO.
  147. flattened_bases = sorted(set(flatten_bases(model)), key=lambda x: model.__mro__.index(x))
  148. # Make our record
  149. bases = tuple(
  150. (
  151. "%s.%s" % (base._meta.app_label, base._meta.model_name)
  152. if hasattr(base, "_meta") else
  153. base
  154. )
  155. for base in flattened_bases
  156. )
  157. # Ensure at least one base inherits from models.Model
  158. if not any((isinstance(base, six.string_types) or issubclass(base, models.Model)) for base in bases):
  159. bases = (models.Model,)
  160. return cls(
  161. model._meta.app_label,
  162. model._meta.object_name,
  163. fields,
  164. options,
  165. bases,
  166. )
  167. def clone(self):
  168. "Returns an exact copy of this ModelState"
  169. # We deep-clone the fields using deconstruction
  170. fields = []
  171. for name, field in self.fields:
  172. _, path, args, kwargs = field.deconstruct()
  173. field_class = import_string(path)
  174. fields.append((name, field_class(*args, **kwargs)))
  175. # Now make a copy
  176. return self.__class__(
  177. app_label=self.app_label,
  178. name=self.name,
  179. fields=fields,
  180. options=dict(self.options),
  181. bases=self.bases,
  182. )
  183. def render(self, apps):
  184. "Creates a Model object from our current state into the given apps"
  185. # First, make a Meta object
  186. meta_contents = {'app_label': self.app_label, "apps": apps}
  187. meta_contents.update(self.options)
  188. if "unique_together" in meta_contents:
  189. meta_contents["unique_together"] = list(meta_contents["unique_together"])
  190. meta = type("Meta", tuple(), meta_contents)
  191. # Then, work out our bases
  192. try:
  193. bases = tuple(
  194. (apps.get_model(base) if isinstance(base, six.string_types) else base)
  195. for base in self.bases
  196. )
  197. except LookupError:
  198. raise InvalidBasesError("Cannot resolve one or more bases from %r" % (self.bases,))
  199. # Turn fields into a dict for the body, add other bits
  200. body = dict(self.fields)
  201. body['Meta'] = meta
  202. body['__module__'] = "__fake__"
  203. # Then, make a Model object
  204. return type(
  205. str(self.name),
  206. bases,
  207. body,
  208. )
  209. def get_field_by_name(self, name):
  210. for fname, field in self.fields:
  211. if fname == name:
  212. return field
  213. raise ValueError("No field called %s on model %s" % (name, self.name))
  214. def __eq__(self, other):
  215. return (
  216. (self.app_label == other.app_label) and
  217. (self.name == other.name) and
  218. (len(self.fields) == len(other.fields)) and
  219. all((k1 == k2 and (f1.deconstruct()[1:] == f2.deconstruct()[1:])) for (k1, f1), (k2, f2) in zip(self.fields, other.fields)) and
  220. (self.options == other.options) and
  221. (self.bases == other.bases)
  222. )
  223. def __ne__(self, other):
  224. return not (self == other)