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.

295 lines
11KB

  1. import os
  2. from .config import _ALLOWED_EXTENSIONS
  3. from .tools import _execute_shell
  4. from .log import logger
  5. from . import tools
  6. import re
  7. import pathlib
  8. import traceback
  9. import inspect
  10. import glob
  11. import yaml
  12. import importlib
  13. class BaseMigrationScript(object):
  14. _TEXT_REPLACES = {}
  15. _TEXT_ERRORS = {}
  16. _TEXT_WARNINGS = {}
  17. _DEPRECATED_MODULES = []
  18. _FILE_RENAMES = {}
  19. _GLOBAL_FUNCTIONS = [] # [function_object]
  20. _module_path = ''
  21. def parse_rules(self):
  22. script_parts = inspect.getfile(self.__class__).split('/')
  23. migrate_from_to = script_parts[-1].split(".")[0]
  24. migration_scripts_dir = "/".join(script_parts[:-1])
  25. TYPE_ARRAY = "TYPE_ARRAY"
  26. TYPE_DICT = "TYPE_DICT"
  27. TYPE_DICT_OF_DICT = "TYPE_DICT_OF_DICT"
  28. rules = {
  29. # {filetype: {regex: replacement}}
  30. "_TEXT_REPLACES": {
  31. "type": TYPE_DICT_OF_DICT,
  32. "doc": {},
  33. },
  34. # {filetype: {regex: message}}
  35. "_TEXT_ERRORS": {
  36. "type": TYPE_DICT_OF_DICT,
  37. "doc": {},
  38. },
  39. # {filetype: {regex: message}}
  40. "_TEXT_WARNINGS": {
  41. "type": TYPE_DICT_OF_DICT,
  42. "doc": {},
  43. },
  44. # [(module, why, ...)]
  45. "_DEPRECATED_MODULES": {
  46. "type": TYPE_ARRAY,
  47. "doc": [],
  48. },
  49. # {old_name: new_name}
  50. "_FILE_RENAMES": {
  51. "type": TYPE_DICT,
  52. "doc": {},
  53. },
  54. }
  55. # read
  56. for rule in rules.keys():
  57. rule_folder = rule[1:].lower()
  58. file_pattern = "%s/%s/%s/*.yaml" % (
  59. migration_scripts_dir,
  60. rule_folder,
  61. migrate_from_to
  62. )
  63. for filename in glob.glob(file_pattern):
  64. with open(filename) as f:
  65. new_rules = yaml.safe_load(f)
  66. if rules[rule]["type"] == TYPE_DICT_OF_DICT:
  67. for f_type, data in new_rules.items():
  68. if f_type not in rules[rule]["doc"]:
  69. rules[rule]["doc"][f_type] = {}
  70. rules[rule]["doc"][f_type].update(data)
  71. elif rules[rule]["type"] == TYPE_DICT:
  72. rules[rule]["doc"].update(new_rules)
  73. elif rules[rule]["type"] == TYPE_ARRAY:
  74. rules[rule]["doc"].extend(new_rules)
  75. # extend
  76. for rule, data in rules.items():
  77. rtype = data["type"]
  78. doc = data.get("doc")
  79. if not doc:
  80. continue
  81. rvalues = getattr(self, rule)
  82. if rtype == TYPE_ARRAY:
  83. rvalues.extend(doc)
  84. elif rtype == TYPE_DICT:
  85. rvalues.update(doc)
  86. else:
  87. # TYPE_DICT_OF_DICT
  88. for filetype, values in doc.items():
  89. rvalues.setdefault(filetype, {})
  90. rvalues[filetype].update(values or {})
  91. file_pattern = "%s/python_scripts/%s/*.py" % (
  92. migration_scripts_dir,
  93. migrate_from_to
  94. )
  95. for path in glob.glob(file_pattern):
  96. module_name = path.split("/")[-1].split(".")[0]
  97. module_name = ".".join([
  98. "odoo_module_migrate.migration_scripts.python_scripts",
  99. migrate_from_to,
  100. module_name,
  101. ])
  102. module = importlib.import_module(module_name)
  103. for name, value in inspect.getmembers(module, inspect.isfunction):
  104. if not name.startswith("_"):
  105. self._GLOBAL_FUNCTIONS.append(value)
  106. def run(self,
  107. module_path,
  108. manifest_path,
  109. module_name,
  110. migration_steps,
  111. directory_path,
  112. commit_enabled):
  113. logger.debug(
  114. 'Running %s script' %
  115. inspect.getfile(self.__class__).split('/')[-1]
  116. )
  117. self.parse_rules()
  118. manifest_path = self._get_correct_manifest_path(
  119. manifest_path,
  120. self._FILE_RENAMES)
  121. for root, directories, filenames in os.walk(module_path.resolve()):
  122. for filename in filenames:
  123. extension = os.path.splitext(filename)[1]
  124. if extension not in _ALLOWED_EXTENSIONS:
  125. continue
  126. self.process_file(
  127. root,
  128. filename,
  129. extension,
  130. self._FILE_RENAMES,
  131. directory_path,
  132. commit_enabled
  133. )
  134. self.handle_deprecated_modules(manifest_path, self._DEPRECATED_MODULES)
  135. if self._GLOBAL_FUNCTIONS:
  136. for function in self._GLOBAL_FUNCTIONS:
  137. function(
  138. logger=logger,
  139. module_path=module_path,
  140. module_name=module_name,
  141. manifest_path=manifest_path,
  142. migration_steps=migration_steps,
  143. tools=tools,
  144. )
  145. def process_file(self,
  146. root,
  147. filename,
  148. extension,
  149. file_renames,
  150. directory_path,
  151. commit_enabled
  152. ):
  153. # Skip useless file
  154. # TODO, skip files present in some folders. (for exemple 'lib')
  155. absolute_file_path = os.path.join(root, filename)
  156. logger.debug("Migrate '%s' file" % absolute_file_path)
  157. # Rename file, if required
  158. new_name = file_renames.get(filename)
  159. if new_name:
  160. self._rename_file(
  161. directory_path,
  162. absolute_file_path,
  163. os.path.join(root, new_name),
  164. commit_enabled
  165. )
  166. absolute_file_path = os.path.join(root, new_name)
  167. # Operate changes in the file (replacements, removals)
  168. replaces = self._TEXT_REPLACES.get("*", {})
  169. replaces.update(self._TEXT_REPLACES.get(extension, {}))
  170. new_text = tools._replace_in_file(
  171. absolute_file_path, replaces,
  172. "Change file content of %s" % filename)
  173. # Display errors if the new content contains some obsolete
  174. # pattern
  175. errors = self._TEXT_ERRORS.get("*", {})
  176. errors.update(self._TEXT_ERRORS.get(extension, {}))
  177. for pattern, error_message in errors.items():
  178. if re.findall(pattern, new_text):
  179. logger.error(error_message)
  180. warnings = self._TEXT_WARNINGS.get("*", {})
  181. warnings.update(self._TEXT_WARNINGS.get(extension, {}))
  182. for pattern, warning_message in warnings.items():
  183. if re.findall(pattern, new_text):
  184. logger.warning(
  185. warning_message +
  186. '. File ' + root + os.sep + filename
  187. )
  188. def handle_deprecated_modules(self, manifest_path, deprecated_modules):
  189. current_manifest_text = tools._read_content(manifest_path)
  190. new_manifest_text = current_manifest_text
  191. for items in deprecated_modules:
  192. old_module, action = items[0:2]
  193. new_module = len(items) > 2 and items[2]
  194. old_module_pattern = r"('|\"){0}('|\")".format(old_module)
  195. if new_module:
  196. new_module_pattern = r"('|\"){0}('|\")".format(new_module)
  197. replace_pattern = r"\1{0}\2".format(new_module)
  198. if not re.findall(old_module_pattern, new_manifest_text):
  199. continue
  200. if action == 'removed':
  201. # The module has been removed, just log an error.
  202. logger.error(
  203. "Depends on removed module '%s'" % (old_module))
  204. elif action == 'renamed':
  205. new_manifest_text = re.sub(
  206. old_module_pattern, replace_pattern, new_manifest_text)
  207. logger.info(
  208. "Replaced dependency of '%s' by '%s'." % (
  209. old_module, new_module))
  210. elif action == 'oca_moved':
  211. new_manifest_text = re.sub(
  212. old_module_pattern, replace_pattern, new_manifest_text)
  213. logger.warning(
  214. "Replaced dependency of '%s' by '%s' (%s)\n"
  215. "Check that '%s' is available on your system." % (
  216. old_module, new_module, items[3], new_module))
  217. elif action == "merged":
  218. if not re.findall(new_module_pattern, new_manifest_text):
  219. # adding dependency of the merged module
  220. new_manifest_text = re.sub(
  221. old_module_pattern, replace_pattern, new_manifest_text)
  222. logger.info(
  223. "'%s' merged in '%s'. Replacing dependency." % (
  224. old_module, new_module))
  225. else:
  226. # TODO, improve me. we should remove the dependency
  227. # but it could generate coma trouble.
  228. # maybe handling this treatment by ast lib could fix
  229. # the problem.
  230. logger.error(
  231. "'%s' merged in '%s'. You should remove the"
  232. " dependency to '%s' manually." % (
  233. old_module, new_module, old_module))
  234. if current_manifest_text != new_manifest_text:
  235. tools._write_content(manifest_path, new_manifest_text)
  236. def _get_correct_manifest_path(self, manifest_path, file_renames):
  237. current_manifest_file_name = manifest_path.as_posix().split('/')[-1]
  238. if current_manifest_file_name in file_renames:
  239. new_manifest_file_name = manifest_path.as_posix().replace(
  240. current_manifest_file_name,
  241. file_renames[current_manifest_file_name]
  242. )
  243. manifest_path = pathlib.Path(new_manifest_file_name)
  244. return manifest_path
  245. def _rename_file(self,
  246. module_path,
  247. old_file_path,
  248. new_file_path,
  249. commit_enabled):
  250. """
  251. Rename a file. try to execute 'git mv', to avoid huge diff.
  252. if 'git mv' fails, make a classical rename
  253. """
  254. logger.info(
  255. "Renaming file: '%s' by '%s' " % (
  256. old_file_path.replace(str(module_path.resolve()), ""),
  257. new_file_path.replace(str(module_path.resolve()), ""))
  258. )
  259. try:
  260. if commit_enabled:
  261. _execute_shell(
  262. "git mv %s %s" % (old_file_path, new_file_path),
  263. path=module_path)
  264. else:
  265. _execute_shell(
  266. "mv %s %s" % (old_file_path, new_file_path),
  267. path=module_path
  268. )
  269. except BaseException:
  270. logger.error(traceback.format_exc())
上海开阖软件有限公司 沪ICP备12045867号-1