south.creator.changes: 250 total statements, 0.0% covered

Generated: Wed 2013-03-13 10:33 CET

Source file: /media/Envs/Envs/filer-gallery/lib/python2.7/site-packages/south/creator/changes.py

Stats: 0 executed, 245 missed, 5 excluded, 239 ignored

  1. """
  2. Contains things to detect changes - either using options passed in on the
  3. commandline, or by using autodetection, etc.
  4. """
  5. from django.db import models
  6. from django.contrib.contenttypes.generic import GenericRelation
  7. from django.utils.datastructures import SortedDict
  8. from south.creator.freezer import remove_useless_attributes, freeze_apps, model_key
  9. from south.utils import auto_through
  10. class BaseChanges(object):
  11. """
  12. Base changes class.
  13. """
  14. def suggest_name(self):
  15. return ''
  16. def split_model_def(self, model, model_def):
  17. """
  18. Given a model and its model def (a dict of field: triple), returns three
  19. items: the real fields dict, the Meta dict, and the M2M fields dict.
  20. """
  21. real_fields = SortedDict()
  22. meta = SortedDict()
  23. m2m_fields = SortedDict()
  24. for name, triple in model_def.items():
  25. if name == "Meta":
  26. meta = triple
  27. elif isinstance(model._meta.get_field_by_name(name)[0], models.ManyToManyField):
  28. m2m_fields[name] = triple
  29. else:
  30. real_fields[name] = triple
  31. return real_fields, meta, m2m_fields
  32. def current_model_from_key(self, key):
  33. app_label, model_name = key.split(".")
  34. return models.get_model(app_label, model_name)
  35. def current_field_from_key(self, key, fieldname):
  36. app_label, model_name = key.split(".")
  37. # Special, for the magical field from order_with_respect_to
  38. if fieldname == "_order":
  39. field = models.IntegerField()
  40. field.name = "_order"
  41. field.attname = "_order"
  42. field.column = "_order"
  43. field.default = 0
  44. return field
  45. # Otherwise, normal.
  46. return models.get_model(app_label, model_name)._meta.get_field_by_name(fieldname)[0]
  47. class AutoChanges(BaseChanges):
  48. """
  49. Detects changes by 'diffing' two sets of frozen model definitions.
  50. """
  51. # Field types we don't generate add/remove field changes for.
  52. IGNORED_FIELD_TYPES = [
  53. GenericRelation,
  54. ]
  55. def __init__(self, migrations, old_defs, old_orm, new_defs):
  56. self.migrations = migrations
  57. self.old_defs = old_defs
  58. self.old_orm = old_orm
  59. self.new_defs = new_defs
  60. def suggest_name(self):
  61. parts = ["auto"]
  62. for change_name, params in self.get_changes():
  63. if change_name == "AddModel":
  64. parts.append("add_%s" % params['model']._meta.object_name.lower())
  65. elif change_name == "DeleteModel":
  66. parts.append("del_%s" % params['model']._meta.object_name.lower())
  67. elif change_name == "AddField":
  68. parts.append("add_field_%s_%s" % (
  69. params['model']._meta.object_name.lower(),
  70. params['field'].name,
  71. ))
  72. elif change_name == "DeleteField":
  73. parts.append("del_field_%s_%s" % (
  74. params['model']._meta.object_name.lower(),
  75. params['field'].name,
  76. ))
  77. elif change_name == "ChangeField":
  78. parts.append("chg_field_%s_%s" % (
  79. params['model']._meta.object_name.lower(),
  80. params['new_field'].name,
  81. ))
  82. elif change_name == "AddUnique":
  83. parts.append("add_unique_%s_%s" % (
  84. params['model']._meta.object_name.lower(),
  85. "_".join([x.name for x in params['fields']]),
  86. ))
  87. elif change_name == "DeleteUnique":
  88. parts.append("del_unique_%s_%s" % (
  89. params['model']._meta.object_name.lower(),
  90. "_".join([x.name for x in params['fields']]),
  91. ))
  92. return ("__".join(parts))[:70]
  93. def get_changes(self):
  94. """
  95. Returns the difference between the old and new sets of models as a 5-tuple:
  96. added_models, deleted_models, added_fields, deleted_fields, changed_fields
  97. """
  98. deleted_models = set()
  99. # See if anything's vanished
  100. for key in self.old_defs:
  101. if key not in self.new_defs:
  102. # We shouldn't delete it if it was managed=False
  103. old_fields, old_meta, old_m2ms = self.split_model_def(self.old_orm[key], self.old_defs[key])
  104. if old_meta.get("managed", "True") != "False":
  105. # Alright, delete it.
  106. yield ("DeleteModel", {
  107. "model": self.old_orm[key],
  108. "model_def": old_fields,
  109. })
  110. # Also make sure we delete any M2Ms it had.
  111. for fieldname in old_m2ms:
  112. # Only delete its stuff if it wasn't a through=.
  113. field = self.old_orm[key + ":" + fieldname]
  114. if auto_through(field):
  115. yield ("DeleteM2M", {"model": self.old_orm[key], "field": field})
  116. # And any unique constraints it had
  117. unique_together = eval(old_meta.get("unique_together", "[]"))
  118. if unique_together:
  119. # If it's only a single tuple, make it into the longer one
  120. if isinstance(unique_together[0], basestring):
  121. unique_together = [unique_together]
  122. # For each combination, make an action for it
  123. for fields in unique_together:
  124. yield ("DeleteUnique", {
  125. "model": self.old_orm[key],
  126. "fields": [self.old_orm[key]._meta.get_field_by_name(x)[0] for x in fields],
  127. })
  128. # We always add it in here so we ignore it later
  129. deleted_models.add(key)
  130. # Or appeared
  131. for key in self.new_defs:
  132. if key not in self.old_defs:
  133. # We shouldn't add it if it's managed=False
  134. new_fields, new_meta, new_m2ms = self.split_model_def(self.current_model_from_key(key), self.new_defs[key])
  135. if new_meta.get("managed", "True") != "False":
  136. yield ("AddModel", {
  137. "model": self.current_model_from_key(key),
  138. "model_def": new_fields,
  139. })
  140. # Also make sure we add any M2Ms it has.
  141. for fieldname in new_m2ms:
  142. # Only create its stuff if it wasn't a through=.
  143. field = self.current_field_from_key(key, fieldname)
  144. if auto_through(field):
  145. yield ("AddM2M", {"model": self.current_model_from_key(key), "field": field})
  146. # And any unique constraints it has
  147. unique_together = eval(new_meta.get("unique_together", "[]"))
  148. if unique_together:
  149. # If it's only a single tuple, make it into the longer one
  150. if isinstance(unique_together[0], basestring):
  151. unique_together = [unique_together]
  152. # For each combination, make an action for it
  153. for fields in unique_together:
  154. yield ("AddUnique", {
  155. "model": self.current_model_from_key(key),
  156. "fields": [self.current_model_from_key(key)._meta.get_field_by_name(x)[0] for x in fields],
  157. })
  158. # Now, for every model that's stayed the same, check its fields.
  159. for key in self.old_defs:
  160. if key not in deleted_models:
  161. old_fields, old_meta, old_m2ms = self.split_model_def(self.old_orm[key], self.old_defs[key])
  162. new_fields, new_meta, new_m2ms = self.split_model_def(self.current_model_from_key(key), self.new_defs[key])
  163. # Do nothing for models which are now not managed.
  164. if new_meta.get("managed", "True") == "False":
  165. continue
  166. # Find fields that have vanished.
  167. for fieldname in old_fields:
  168. if fieldname not in new_fields:
  169. # Don't do it for any fields we're ignoring
  170. field = self.old_orm[key + ":" + fieldname]
  171. field_allowed = True
  172. for field_type in self.IGNORED_FIELD_TYPES:
  173. if isinstance(field, field_type):
  174. field_allowed = False
  175. if field_allowed:
  176. # Looks alright.
  177. yield ("DeleteField", {
  178. "model": self.old_orm[key],
  179. "field": field,
  180. "field_def": old_fields[fieldname],
  181. })
  182. # And ones that have appeared
  183. for fieldname in new_fields:
  184. if fieldname not in old_fields:
  185. # Don't do it for any fields we're ignoring
  186. field = self.current_field_from_key(key, fieldname)
  187. field_allowed = True
  188. for field_type in self.IGNORED_FIELD_TYPES:
  189. if isinstance(field, field_type):
  190. field_allowed = False
  191. if field_allowed:
  192. # Looks alright.
  193. yield ("AddField", {
  194. "model": self.current_model_from_key(key),
  195. "field": field,
  196. "field_def": new_fields[fieldname],
  197. })
  198. # Find M2Ms that have vanished
  199. for fieldname in old_m2ms:
  200. if fieldname not in new_m2ms:
  201. # Only delete its stuff if it wasn't a through=.
  202. field = self.old_orm[key + ":" + fieldname]
  203. if auto_through(field):
  204. yield ("DeleteM2M", {"model": self.old_orm[key], "field": field})
  205. # Find M2Ms that have appeared
  206. for fieldname in new_m2ms:
  207. if fieldname not in old_m2ms:
  208. # Only create its stuff if it wasn't a through=.
  209. field = self.current_field_from_key(key, fieldname)
  210. if auto_through(field):
  211. yield ("AddM2M", {"model": self.current_model_from_key(key), "field": field})
  212. # For the ones that exist in both models, see if they were changed
  213. for fieldname in set(old_fields).intersection(set(new_fields)):
  214. # Non-index changes
  215. if self.different_attributes(
  216. remove_useless_attributes(old_fields[fieldname], True, True),
  217. remove_useless_attributes(new_fields[fieldname], True, True)):
  218. yield ("ChangeField", {
  219. "model": self.current_model_from_key(key),
  220. "old_field": self.old_orm[key + ":" + fieldname],
  221. "new_field": self.current_field_from_key(key, fieldname),
  222. "old_def": old_fields[fieldname],
  223. "new_def": new_fields[fieldname],
  224. })
  225. # Index changes
  226. old_field = self.old_orm[key + ":" + fieldname]
  227. new_field = self.current_field_from_key(key, fieldname)
  228. if not old_field.db_index and new_field.db_index:
  229. # They've added an index.
  230. yield ("AddIndex", {
  231. "model": self.current_model_from_key(key),
  232. "fields": [new_field],
  233. })
  234. if old_field.db_index and not new_field.db_index:
  235. # They've removed an index.
  236. yield ("DeleteIndex", {
  237. "model": self.old_orm[key],
  238. "fields": [old_field],
  239. })
  240. # See if their uniques have changed
  241. if old_field.unique != new_field.unique:
  242. # Make sure we look at the one explicitly given to see what happened
  243. if new_field.unique:
  244. yield ("AddUnique", {
  245. "model": self.current_model_from_key(key),
  246. "fields": [new_field],
  247. })
  248. else:
  249. yield ("DeleteUnique", {
  250. "model": self.old_orm[key],
  251. "fields": [old_field],
  252. })
  253. # See if there's any M2Ms that have changed.
  254. for fieldname in set(old_m2ms).intersection(set(new_m2ms)):
  255. old_field = self.old_orm[key + ":" + fieldname]
  256. new_field = self.current_field_from_key(key, fieldname)
  257. # Have they _added_ a through= ?
  258. if auto_through(old_field) and not auto_through(new_field):
  259. yield ("DeleteM2M", {"model": self.old_orm[key], "field": old_field})
  260. # Have they _removed_ a through= ?
  261. if not auto_through(old_field) and auto_through(new_field):
  262. yield ("AddM2M", {"model": self.current_model_from_key(key), "field": new_field})
  263. ## See if the unique_togethers have changed
  264. # First, normalise them into lists of sets.
  265. old_unique_together = eval(old_meta.get("unique_together", "[]"))
  266. new_unique_together = eval(new_meta.get("unique_together", "[]"))
  267. if old_unique_together and isinstance(old_unique_together[0], basestring):
  268. old_unique_together = [old_unique_together]
  269. if new_unique_together and isinstance(new_unique_together[0], basestring):
  270. new_unique_together = [new_unique_together]
  271. old_unique_together = map(set, old_unique_together)
  272. new_unique_together = map(set, new_unique_together)
  273. # See if any appeared or disappeared
  274. for item in old_unique_together:
  275. if item not in new_unique_together:
  276. yield ("DeleteUnique", {
  277. "model": self.old_orm[key],
  278. "fields": [self.old_orm[key + ":" + x] for x in item],
  279. })
  280. for item in new_unique_together:
  281. if item not in old_unique_together:
  282. yield ("AddUnique", {
  283. "model": self.current_model_from_key(key),
  284. "fields": [self.current_field_from_key(key, x) for x in item],
  285. })
  286. @classmethod
  287. def is_triple(cls, triple):
  288. "Returns whether the argument is a triple."
  289. return isinstance(triple, (list, tuple)) and len(triple) == 3 and \
  290. isinstance(triple[0], (str, unicode)) and \
  291. isinstance(triple[1], (list, tuple)) and \
  292. isinstance(triple[2], dict)
  293. @classmethod
  294. def different_attributes(cls, old, new):
  295. """
  296. Backwards-compat comparison that ignores orm. on the RHS and not the left
  297. and which knows django.db.models.fields.CharField = models.CharField.
  298. Has a whole load of tests in tests/autodetection.py.
  299. """
  300. # If they're not triples, just do normal comparison
  301. if not cls.is_triple(old) or not cls.is_triple(new):
  302. return old != new
  303. # Expand them out into parts
  304. old_field, old_pos, old_kwd = old
  305. new_field, new_pos, new_kwd = new
  306. # Copy the positional and keyword arguments so we can compare them and pop off things
  307. old_pos, new_pos = old_pos[:], new_pos[:]
  308. old_kwd = dict(old_kwd.items())
  309. new_kwd = dict(new_kwd.items())
  310. # Remove comparison of the existence of 'unique', that's done elsewhere.
  311. # TODO: Make this work for custom fields where unique= means something else?
  312. if "unique" in old_kwd:
  313. del old_kwd['unique']
  314. if "unique" in new_kwd:
  315. del new_kwd['unique']
  316. # If the first bit is different, check it's not by dj.db.models...
  317. if old_field != new_field:
  318. if old_field.startswith("models.") and (new_field.startswith("django.db.models") \
  319. or new_field.startswith("django.contrib.gis")):
  320. if old_field.split(".")[-1] != new_field.split(".")[-1]:
  321. return True
  322. else:
  323. # Remove those fields from the final comparison
  324. old_field = new_field = ""
  325. # If there's a positional argument in the first, and a 'to' in the second,
  326. # see if they're actually comparable.
  327. if (old_pos and "to" in new_kwd) and ("orm" in new_kwd['to'] and "orm" not in old_pos[0]):
  328. # Do special comparison to fix #153
  329. try:
  330. if old_pos[0] != new_kwd['to'].split("'")[1].split(".")[1]:
  331. return True
  332. except IndexError:
  333. pass # Fall back to next comparison
  334. # Remove those attrs from the final comparison
  335. old_pos = old_pos[1:]
  336. del new_kwd['to']
  337. return old_field != new_field or old_pos != new_pos or old_kwd != new_kwd
  338. class ManualChanges(BaseChanges):
  339. """
  340. Detects changes by reading the command line.
  341. """
  342. def __init__(self, migrations, added_models, added_fields, added_indexes):
  343. self.migrations = migrations
  344. self.added_models = added_models
  345. self.added_fields = added_fields
  346. self.added_indexes = added_indexes
  347. def suggest_name(self):
  348. bits = []
  349. for model_name in self.added_models:
  350. bits.append('add_model_%s' % model_name)
  351. for field_name in self.added_fields:
  352. bits.append('add_field_%s' % field_name)
  353. for index_name in self.added_indexes:
  354. bits.append('add_index_%s' % index_name)
  355. return '_'.join(bits).replace('.', '_')
  356. def get_changes(self):
  357. # Get the model defs so we can use them for the yield later
  358. model_defs = freeze_apps([self.migrations.app_label()])
  359. # Make the model changes
  360. for model_name in self.added_models:
  361. model = models.get_model(self.migrations.app_label(), model_name)
  362. real_fields, meta, m2m_fields = self.split_model_def(model, model_defs[model_key(model)])
  363. yield ("AddModel", {
  364. "model": model,
  365. "model_def": real_fields,
  366. })
  367. # And the field changes
  368. for field_desc in self.added_fields:
  369. try:
  370. model_name, field_name = field_desc.split(".")
  371. except (TypeError, ValueError):
  372. raise ValueError("%r is not a valid field description." % field_desc)
  373. model = models.get_model(self.migrations.app_label(), model_name)
  374. real_fields, meta, m2m_fields = self.split_model_def(model, model_defs[model_key(model)])
  375. yield ("AddField", {
  376. "model": model,
  377. "field": model._meta.get_field_by_name(field_name)[0],
  378. "field_def": real_fields[field_name],
  379. })
  380. # And the indexes
  381. for field_desc in self.added_indexes:
  382. try:
  383. model_name, field_name = field_desc.split(".")
  384. except (TypeError, ValueError):
  385. print "%r is not a valid field description." % field_desc
  386. model = models.get_model(self.migrations.app_label(), model_name)
  387. yield ("AddIndex", {
  388. "model": model,
  389. "fields": [model._meta.get_field_by_name(field_name)[0]],
  390. })
  391. class InitialChanges(BaseChanges):
  392. """
  393. Creates all models; handles --initial.
  394. """
  395. def suggest_name(self):
  396. return 'initial'
  397. def __init__(self, migrations):
  398. self.migrations = migrations
  399. def get_changes(self):
  400. # Get the frozen models for this app
  401. model_defs = freeze_apps([self.migrations.app_label()])
  402. for model in models.get_models(models.get_app(self.migrations.app_label())):
  403. # Don't do anything for unmanaged, abstract or proxy models
  404. if model._meta.abstract or getattr(model._meta, "proxy", False) or not getattr(model._meta, "managed", True):
  405. continue
  406. real_fields, meta, m2m_fields = self.split_model_def(model, model_defs[model_key(model)])
  407. # Firstly, add the main table and fields
  408. yield ("AddModel", {
  409. "model": model,
  410. "model_def": real_fields,
  411. })
  412. # Then, add any uniqueness that's around
  413. if meta:
  414. unique_together = eval(meta.get("unique_together", "[]"))
  415. if unique_together:
  416. # If it's only a single tuple, make it into the longer one
  417. if isinstance(unique_together[0], basestring):
  418. unique_together = [unique_together]
  419. # For each combination, make an action for it
  420. for fields in unique_together:
  421. yield ("AddUnique", {
  422. "model": model,
  423. "fields": [model._meta.get_field_by_name(x)[0] for x in fields],
  424. })
  425. # Finally, see if there's some M2M action
  426. for name, triple in m2m_fields.items():
  427. field = model._meta.get_field_by_name(name)[0]
  428. # But only if it's not through=foo (#120)
  429. if field.rel.through:
  430. try:
  431. # Django 1.1 and below
  432. through_model = field.rel.through_model
  433. except AttributeError:
  434. # Django 1.2
  435. through_model = field.rel.through
  436. if (not field.rel.through) or getattr(through_model._meta, "auto_created", False):
  437. yield ("AddM2M", {
  438. "model": model,
  439. "field": field,
  440. })