state.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. from __future__ import unicode_literals
  2. from django.apps import AppConfig
  3. from django.apps.registry import Apps, apps as global_apps
  4. from django.db import models
  5. from django.db.models.options import DEFAULT_NAMES, normalize_together
  6. from django.db.models.fields.related import do_pending_lookups
  7. from django.db.models.fields.proxy import OrderWrt
  8. from django.conf import settings
  9. from django.utils import six
  10. from django.utils.encoding import force_text, smart_text
  11. from django.utils.module_loading import import_string
  12. class InvalidBasesError(ValueError):
  13. pass
  14. class ProjectState(object):
  15. """
  16. Represents the entire project's overall state.
  17. This is the item that is passed around - we do it here rather than at the
  18. app level so that cross-app FKs/etc. resolve properly.
  19. """
  20. def __init__(self, models=None, real_apps=None):
  21. self.models = models or {}
  22. self.apps = None
  23. # Apps to include from main registry, usually unmigrated ones
  24. self.real_apps = real_apps or []
  25. def add_model_state(self, model_state):
  26. self.models[(model_state.app_label, model_state.name.lower())] = model_state
  27. def clone(self):
  28. "Returns an exact copy of this ProjectState"
  29. return ProjectState(
  30. models=dict((k, v.clone()) for k, v in self.models.items()),
  31. real_apps=self.real_apps,
  32. )
  33. def render(self, include_real=None, ignore_swappable=False, skip_cache=False):
  34. "Turns the project state into actual models in a new Apps"
  35. if self.apps is None or skip_cache:
  36. # Any apps in self.real_apps should have all their models included
  37. # in the render. We don't use the original model instances as there
  38. # are some variables that refer to the Apps object.
  39. # FKs/M2Ms from real apps are also not included as they just
  40. # mess things up with partial states (due to lack of dependencies)
  41. real_models = []
  42. for app_label in self.real_apps:
  43. app = global_apps.get_app_config(app_label)
  44. for model in app.get_models():
  45. real_models.append(ModelState.from_model(model, exclude_rels=True))
  46. # Populate the app registry with a stub for each application.
  47. app_labels = set(model_state.app_label for model_state in self.models.values())
  48. self.apps = Apps([AppConfigStub(label) for label in sorted(self.real_apps + list(app_labels))])
  49. # We keep trying to render the models in a loop, ignoring invalid
  50. # base errors, until the size of the unrendered models doesn't
  51. # decrease by at least one, meaning there's a base dependency loop/
  52. # missing base.
  53. unrendered_models = list(self.models.values()) + real_models
  54. while unrendered_models:
  55. new_unrendered_models = []
  56. for model in unrendered_models:
  57. try:
  58. model.render(self.apps)
  59. except InvalidBasesError:
  60. new_unrendered_models.append(model)
  61. if len(new_unrendered_models) == len(unrendered_models):
  62. raise InvalidBasesError("Cannot resolve bases for %r\nThis can happen if you are inheriting models from an app with migrations (e.g. contrib.auth)\n in an app with no migrations; see https://docs.djangoproject.com/en/1.7/topics/migrations/#dependencies for more" % new_unrendered_models)
  63. unrendered_models = new_unrendered_models
  64. # make sure apps has no dangling references
  65. if self.apps._pending_lookups:
  66. # There's some lookups left. See if we can first resolve them
  67. # ourselves - sometimes fields are added after class_prepared is sent
  68. for lookup_model, operations in self.apps._pending_lookups.items():
  69. try:
  70. model = self.apps.get_model(lookup_model[0], lookup_model[1])
  71. except LookupError:
  72. if "%s.%s" % (lookup_model[0], lookup_model[1]) == settings.AUTH_USER_MODEL and ignore_swappable:
  73. continue
  74. # Raise an error with a best-effort helpful message
  75. # (only for the first issue). Error message should look like:
  76. # "ValueError: Lookup failed for model referenced by
  77. # field migrations.Book.author: migrations.Author"
  78. raise ValueError("Lookup failed for model referenced by field {field}: {model[0]}.{model[1]}".format(
  79. field=operations[0][1],
  80. model=lookup_model,
  81. ))
  82. else:
  83. do_pending_lookups(model)
  84. try:
  85. return self.apps
  86. finally:
  87. if skip_cache:
  88. self.apps = None
  89. @classmethod
  90. def from_apps(cls, apps):
  91. "Takes in an Apps and returns a ProjectState matching it"
  92. app_models = {}
  93. for model in apps.get_models(include_swapped=True):
  94. model_state = ModelState.from_model(model)
  95. app_models[(model_state.app_label, model_state.name.lower())] = model_state
  96. return cls(app_models)
  97. def __eq__(self, other):
  98. if set(self.models.keys()) != set(other.models.keys()):
  99. return False
  100. if set(self.real_apps) != set(other.real_apps):
  101. return False
  102. return all(model == other.models[key] for key, model in self.models.items())
  103. def __ne__(self, other):
  104. return not (self == other)
  105. class AppConfigStub(AppConfig):
  106. """
  107. Stubs a Django AppConfig. Only provides a label, and a dict of models.
  108. """
  109. # Not used, but required by AppConfig.__init__
  110. path = ''
  111. def __init__(self, label):
  112. super(AppConfigStub, self).__init__(label, None)
  113. def import_models(self, all_models):
  114. self.models = all_models
  115. class ModelState(object):
  116. """
  117. Represents a Django Model. We don't use the actual Model class
  118. as it's not designed to have its options changed - instead, we
  119. mutate this one and then render it into a Model as required.
  120. Note that while you are allowed to mutate .fields, you are not allowed
  121. to mutate the Field instances inside there themselves - you must instead
  122. assign new ones, as these are not detached during a clone.
  123. """
  124. def __init__(self, app_label, name, fields, options=None, bases=None):
  125. self.app_label = app_label
  126. self.name = force_text(name)
  127. self.fields = fields
  128. self.options = options or {}
  129. self.bases = bases or (models.Model, )
  130. # Sanity-check that fields is NOT a dict. It must be ordered.
  131. if isinstance(self.fields, dict):
  132. raise ValueError("ModelState.fields cannot be a dict - it must be a list of 2-tuples.")
  133. # Sanity-check that fields are NOT already bound to a model.
  134. for name, field in fields:
  135. if hasattr(field, 'model'):
  136. raise ValueError(
  137. 'ModelState.fields cannot be bound to a model - "%s" is.' % name
  138. )
  139. @classmethod
  140. def from_model(cls, model, exclude_rels=False):
  141. """
  142. Feed me a model, get a ModelState representing it out.
  143. """
  144. # Deconstruct the fields
  145. fields = []
  146. for field in model._meta.local_fields:
  147. if getattr(field, "rel", None) and exclude_rels:
  148. continue
  149. if isinstance(field, OrderWrt):
  150. continue
  151. name, path, args, kwargs = field.deconstruct()
  152. field_class = import_string(path)
  153. try:
  154. fields.append((name, field_class(*args, **kwargs)))
  155. except TypeError as e:
  156. raise TypeError("Couldn't reconstruct field %s on %s.%s: %s" % (
  157. name,
  158. model._meta.app_label,
  159. model._meta.object_name,
  160. e,
  161. ))
  162. if not exclude_rels:
  163. for field in model._meta.local_many_to_many:
  164. name, path, args, kwargs = field.deconstruct()
  165. field_class = import_string(path)
  166. try:
  167. fields.append((name, field_class(*args, **kwargs)))
  168. except TypeError as e:
  169. raise TypeError("Couldn't reconstruct m2m field %s on %s: %s" % (
  170. name,
  171. model._meta.object_name,
  172. e,
  173. ))
  174. # Extract the options
  175. options = {}
  176. for name in DEFAULT_NAMES:
  177. # Ignore some special options
  178. if name in ["apps", "app_label"]:
  179. continue
  180. elif name in model._meta.original_attrs:
  181. if name == "unique_together":
  182. ut = model._meta.original_attrs["unique_together"]
  183. options[name] = set(normalize_together(ut))
  184. elif name == "index_together":
  185. it = model._meta.original_attrs["index_together"]
  186. options[name] = set(normalize_together(it))
  187. else:
  188. options[name] = model._meta.original_attrs[name]
  189. # Force-convert all options to text_type (#23226)
  190. options = cls.force_text_recursive(options)
  191. # If we're ignoring relationships, remove all field-listing model
  192. # options (that option basically just means "make a stub model")
  193. if exclude_rels:
  194. for key in ["unique_together", "index_together", "order_with_respect_to"]:
  195. if key in options:
  196. del options[key]
  197. def flatten_bases(model):
  198. bases = []
  199. for base in model.__bases__:
  200. if hasattr(base, "_meta") and base._meta.abstract:
  201. bases.extend(flatten_bases(base))
  202. else:
  203. bases.append(base)
  204. return bases
  205. # We can't rely on __mro__ directly because we only want to flatten
  206. # abstract models and not the whole tree. However by recursing on
  207. # __bases__ we may end up with duplicates and ordering issues, we
  208. # therefore discard any duplicates and reorder the bases according
  209. # to their index in the MRO.
  210. flattened_bases = sorted(set(flatten_bases(model)), key=lambda x: model.__mro__.index(x))
  211. # Make our record
  212. bases = tuple(
  213. (
  214. "%s.%s" % (base._meta.app_label, base._meta.model_name)
  215. if hasattr(base, "_meta") else
  216. base
  217. )
  218. for base in flattened_bases
  219. )
  220. # Ensure at least one base inherits from models.Model
  221. if not any((isinstance(base, six.string_types) or issubclass(base, models.Model)) for base in bases):
  222. bases = (models.Model,)
  223. return cls(
  224. model._meta.app_label,
  225. model._meta.object_name,
  226. fields,
  227. options,
  228. bases,
  229. )
  230. @classmethod
  231. def force_text_recursive(cls, value):
  232. if isinstance(value, six.string_types):
  233. return smart_text(value)
  234. elif isinstance(value, list):
  235. return [cls.force_text_recursive(x) for x in value]
  236. elif isinstance(value, tuple):
  237. return tuple(cls.force_text_recursive(x) for x in value)
  238. elif isinstance(value, set):
  239. return set(cls.force_text_recursive(x) for x in value)
  240. elif isinstance(value, dict):
  241. return dict(
  242. (cls.force_text_recursive(k), cls.force_text_recursive(v))
  243. for k, v in value.items()
  244. )
  245. return value
  246. def construct_fields(self):
  247. "Deep-clone the fields using deconstruction"
  248. for name, field in self.fields:
  249. _, path, args, kwargs = field.deconstruct()
  250. field_class = import_string(path)
  251. yield name, field_class(*args, **kwargs)
  252. def clone(self):
  253. "Returns an exact copy of this ModelState"
  254. return self.__class__(
  255. app_label=self.app_label,
  256. name=self.name,
  257. fields=list(self.construct_fields()),
  258. options=dict(self.options),
  259. bases=self.bases,
  260. )
  261. def render(self, apps):
  262. "Creates a Model object from our current state into the given apps"
  263. # First, make a Meta object
  264. meta_contents = {'app_label': self.app_label, "apps": apps}
  265. meta_contents.update(self.options)
  266. meta = type(str("Meta"), tuple(), meta_contents)
  267. # Then, work out our bases
  268. try:
  269. bases = tuple(
  270. (apps.get_model(base) if isinstance(base, six.string_types) else base)
  271. for base in self.bases
  272. )
  273. except LookupError:
  274. raise InvalidBasesError("Cannot resolve one or more bases from %r" % (self.bases,))
  275. # Turn fields into a dict for the body, add other bits
  276. body = dict(self.construct_fields())
  277. body['Meta'] = meta
  278. body['__module__'] = "__fake__"
  279. # Then, make a Model object
  280. return type(
  281. str(self.name),
  282. bases,
  283. body,
  284. )
  285. def get_field_by_name(self, name):
  286. for fname, field in self.fields:
  287. if fname == name:
  288. return field
  289. raise ValueError("No field called %s on model %s" % (name, self.name))
  290. def __repr__(self):
  291. return "<ModelState: '%s.%s'>" % (self.app_label, self.name)
  292. def __eq__(self, other):
  293. return (
  294. (self.app_label == other.app_label) and
  295. (self.name == other.name) and
  296. (len(self.fields) == len(other.fields)) and
  297. all((k1 == k2 and (f1.deconstruct()[1:] == f2.deconstruct()[1:])) for (k1, f1), (k2, f2) in zip(self.fields, other.fields)) and
  298. (self.options == other.options) and
  299. (self.bases == other.bases)
  300. )
  301. def __ne__(self, other):
  302. return not (self == other)