Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

writer.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. import os
  2. import re
  3. from importlib import import_module
  4. from django import get_version
  5. from django.apps import apps
  6. from django.db import migrations
  7. from django.db.migrations.loader import MigrationLoader
  8. from django.db.migrations.serializer import serializer_factory
  9. from django.utils.inspect import get_func_args
  10. from django.utils.module_loading import module_dir
  11. from django.utils.timezone import now
  12. class SettingsReference(str):
  13. """
  14. Special subclass of string which actually references a current settings
  15. value. It's treated as the value in memory, but serializes out to a
  16. settings.NAME attribute reference.
  17. """
  18. def __new__(self, value, setting_name):
  19. return str.__new__(self, value)
  20. def __init__(self, value, setting_name):
  21. self.setting_name = setting_name
  22. class OperationWriter:
  23. def __init__(self, operation, indentation=2):
  24. self.operation = operation
  25. self.buff = []
  26. self.indentation = indentation
  27. def serialize(self):
  28. def _write(_arg_name, _arg_value):
  29. if (_arg_name in self.operation.serialization_expand_args and
  30. isinstance(_arg_value, (list, tuple, dict))):
  31. if isinstance(_arg_value, dict):
  32. self.feed('%s={' % _arg_name)
  33. self.indent()
  34. for key, value in _arg_value.items():
  35. key_string, key_imports = MigrationWriter.serialize(key)
  36. arg_string, arg_imports = MigrationWriter.serialize(value)
  37. args = arg_string.splitlines()
  38. if len(args) > 1:
  39. self.feed('%s: %s' % (key_string, args[0]))
  40. for arg in args[1:-1]:
  41. self.feed(arg)
  42. self.feed('%s,' % args[-1])
  43. else:
  44. self.feed('%s: %s,' % (key_string, arg_string))
  45. imports.update(key_imports)
  46. imports.update(arg_imports)
  47. self.unindent()
  48. self.feed('},')
  49. else:
  50. self.feed('%s=[' % _arg_name)
  51. self.indent()
  52. for item in _arg_value:
  53. arg_string, arg_imports = MigrationWriter.serialize(item)
  54. args = arg_string.splitlines()
  55. if len(args) > 1:
  56. for arg in args[:-1]:
  57. self.feed(arg)
  58. self.feed('%s,' % args[-1])
  59. else:
  60. self.feed('%s,' % arg_string)
  61. imports.update(arg_imports)
  62. self.unindent()
  63. self.feed('],')
  64. else:
  65. arg_string, arg_imports = MigrationWriter.serialize(_arg_value)
  66. args = arg_string.splitlines()
  67. if len(args) > 1:
  68. self.feed('%s=%s' % (_arg_name, args[0]))
  69. for arg in args[1:-1]:
  70. self.feed(arg)
  71. self.feed('%s,' % args[-1])
  72. else:
  73. self.feed('%s=%s,' % (_arg_name, arg_string))
  74. imports.update(arg_imports)
  75. imports = set()
  76. name, args, kwargs = self.operation.deconstruct()
  77. operation_args = get_func_args(self.operation.__init__)
  78. # See if this operation is in django.db.migrations. If it is,
  79. # We can just use the fact we already have that imported,
  80. # otherwise, we need to add an import for the operation class.
  81. if getattr(migrations, name, None) == self.operation.__class__:
  82. self.feed('migrations.%s(' % name)
  83. else:
  84. imports.add('import %s' % (self.operation.__class__.__module__))
  85. self.feed('%s.%s(' % (self.operation.__class__.__module__, name))
  86. self.indent()
  87. for i, arg in enumerate(args):
  88. arg_value = arg
  89. arg_name = operation_args[i]
  90. _write(arg_name, arg_value)
  91. i = len(args)
  92. # Only iterate over remaining arguments
  93. for arg_name in operation_args[i:]:
  94. if arg_name in kwargs: # Don't sort to maintain signature order
  95. arg_value = kwargs[arg_name]
  96. _write(arg_name, arg_value)
  97. self.unindent()
  98. self.feed('),')
  99. return self.render(), imports
  100. def indent(self):
  101. self.indentation += 1
  102. def unindent(self):
  103. self.indentation -= 1
  104. def feed(self, line):
  105. self.buff.append(' ' * (self.indentation * 4) + line)
  106. def render(self):
  107. return '\n'.join(self.buff)
  108. class MigrationWriter:
  109. """
  110. Take a Migration instance and is able to produce the contents
  111. of the migration file from it.
  112. """
  113. def __init__(self, migration):
  114. self.migration = migration
  115. self.needs_manual_porting = False
  116. def as_string(self):
  117. """Return a string of the file contents."""
  118. items = {
  119. "replaces_str": "",
  120. "initial_str": "",
  121. }
  122. imports = set()
  123. # Deconstruct operations
  124. operations = []
  125. for operation in self.migration.operations:
  126. operation_string, operation_imports = OperationWriter(operation).serialize()
  127. imports.update(operation_imports)
  128. operations.append(operation_string)
  129. items["operations"] = "\n".join(operations) + "\n" if operations else ""
  130. # Format dependencies and write out swappable dependencies right
  131. dependencies = []
  132. for dependency in self.migration.dependencies:
  133. if dependency[0] == "__setting__":
  134. dependencies.append(" migrations.swappable_dependency(settings.%s)," % dependency[1])
  135. imports.add("from django.conf import settings")
  136. else:
  137. dependencies.append(" %s," % self.serialize(dependency)[0])
  138. items["dependencies"] = "\n".join(dependencies) + "\n" if dependencies else ""
  139. # Format imports nicely, swapping imports of functions from migration files
  140. # for comments
  141. migration_imports = set()
  142. for line in list(imports):
  143. if re.match(r"^import (.*)\.\d+[^\s]*$", line):
  144. migration_imports.add(line.split("import")[1].strip())
  145. imports.remove(line)
  146. self.needs_manual_porting = True
  147. # django.db.migrations is always used, but models import may not be.
  148. # If models import exists, merge it with migrations import.
  149. if "from django.db import models" in imports:
  150. imports.discard("from django.db import models")
  151. imports.add("from django.db import migrations, models")
  152. else:
  153. imports.add("from django.db import migrations")
  154. # Sort imports by the package / module to be imported (the part after
  155. # "from" in "from ... import ..." or after "import" in "import ...").
  156. sorted_imports = sorted(imports, key=lambda i: i.split()[1])
  157. items["imports"] = "\n".join(sorted_imports) + "\n" if imports else ""
  158. if migration_imports:
  159. items["imports"] += (
  160. "\n\n# Functions from the following migrations need manual "
  161. "copying.\n# Move them and any dependencies into this file, "
  162. "then update the\n# RunPython operations to refer to the local "
  163. "versions:\n# %s"
  164. ) % "\n# ".join(sorted(migration_imports))
  165. # If there's a replaces, make a string for it
  166. if self.migration.replaces:
  167. items['replaces_str'] = "\n replaces = %s\n" % self.serialize(self.migration.replaces)[0]
  168. # Hinting that goes into comment
  169. items.update(
  170. version=get_version(),
  171. timestamp=now().strftime("%Y-%m-%d %H:%M"),
  172. )
  173. if self.migration.initial:
  174. items['initial_str'] = "\n initial = True\n"
  175. return MIGRATION_TEMPLATE % items
  176. @property
  177. def basedir(self):
  178. migrations_package_name, _ = MigrationLoader.migrations_module(self.migration.app_label)
  179. if migrations_package_name is None:
  180. raise ValueError(
  181. "Django can't create migrations for app '%s' because "
  182. "migrations have been disabled via the MIGRATION_MODULES "
  183. "setting." % self.migration.app_label
  184. )
  185. # See if we can import the migrations module directly
  186. try:
  187. migrations_module = import_module(migrations_package_name)
  188. except ImportError:
  189. pass
  190. else:
  191. try:
  192. return module_dir(migrations_module)
  193. except ValueError:
  194. pass
  195. # Alright, see if it's a direct submodule of the app
  196. app_config = apps.get_app_config(self.migration.app_label)
  197. maybe_app_name, _, migrations_package_basename = migrations_package_name.rpartition(".")
  198. if app_config.name == maybe_app_name:
  199. return os.path.join(app_config.path, migrations_package_basename)
  200. # In case of using MIGRATION_MODULES setting and the custom package
  201. # doesn't exist, create one, starting from an existing package
  202. existing_dirs, missing_dirs = migrations_package_name.split("."), []
  203. while existing_dirs:
  204. missing_dirs.insert(0, existing_dirs.pop(-1))
  205. try:
  206. base_module = import_module(".".join(existing_dirs))
  207. except (ImportError, ValueError):
  208. continue
  209. else:
  210. try:
  211. base_dir = module_dir(base_module)
  212. except ValueError:
  213. continue
  214. else:
  215. break
  216. else:
  217. raise ValueError(
  218. "Could not locate an appropriate location to create "
  219. "migrations package %s. Make sure the toplevel "
  220. "package exists and can be imported." %
  221. migrations_package_name)
  222. final_dir = os.path.join(base_dir, *missing_dirs)
  223. if not os.path.isdir(final_dir):
  224. os.makedirs(final_dir)
  225. for missing_dir in missing_dirs:
  226. base_dir = os.path.join(base_dir, missing_dir)
  227. with open(os.path.join(base_dir, "__init__.py"), "w"):
  228. pass
  229. return final_dir
  230. @property
  231. def filename(self):
  232. return "%s.py" % self.migration.name
  233. @property
  234. def path(self):
  235. return os.path.join(self.basedir, self.filename)
  236. @classmethod
  237. def serialize(cls, value):
  238. return serializer_factory(value).serialize()
  239. MIGRATION_TEMPLATE = """\
  240. # Generated by Django %(version)s on %(timestamp)s
  241. %(imports)s
  242. class Migration(migrations.Migration):
  243. %(replaces_str)s%(initial_str)s
  244. dependencies = [
  245. %(dependencies)s\
  246. ]
  247. operations = [
  248. %(operations)s\
  249. ]
  250. """