PageRenderTime 51ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/python/rose/macros/rule.py

https://github.com/hjoliver/rose
Python | 284 lines | 258 code | 6 blank | 20 comment | 2 complexity | 6fac50c1d4368c433eb209761ef2e97c MD5 | raw file
Possible License(s): GPL-3.0, BSD-3-Clause, GPL-2.0
  1. # -*- coding: utf-8 -*-
  2. #-----------------------------------------------------------------------------
  3. # (C) British Crown Copyright 2012-4 Met Office.
  4. #
  5. # This file is part of Rose, a framework for meteorological suites.
  6. #
  7. # Rose is free software: you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation, either version 3 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # Rose is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License
  18. # along with Rose. If not, see <http://www.gnu.org/licenses/>.
  19. #-----------------------------------------------------------------------------
  20. import ast
  21. import os
  22. import re
  23. import sys
  24. import jinja2
  25. import rose.macro
  26. import rose.variable
  27. REC_EXPR_IS_THIS_RULE = re.compile(
  28. """(?:^.*[^\w:=]|^) (?# Break or beginning)
  29. this (?# 'this')
  30. (?: (?# Followed by:)
  31. $ (?# the end)
  32. | (?# or)
  33. \W (?# break plus)
  34. .* (?# anything)
  35. ( (?# Start operator)
  36. [+*%<>=-] (?# Arithmetic)
  37. | (?# or)
  38. in\s (?# String)
  39. | (?# or)
  40. not\s (?# Logical not)
  41. | (?# or)
  42. and\s (?# Logical and)
  43. | (?# or)
  44. or\s (?# Logical or)
  45. ) (?# End operator)
  46. .* (?# anything)
  47. $ (?# the end)
  48. )""", re.X)
  49. class RuleValueError(Exception):
  50. def __init__(self, *args):
  51. self.args = args
  52. def __str__(self):
  53. arg_string = " ".join([str(a) for a in self.args])
  54. return "{0} - could not retrieve value. ".format(arg_string)
  55. class FailureRuleChecker(rose.macro.MacroBase):
  56. """Check the fail-if and warn-if conditions"""
  57. ERROR_RULE_FAILED = "failed because"
  58. REC_RULE_SPLIT = re.compile(r"\s*;\s*")
  59. RULE_ERROR_NAME = "fail-if"
  60. RULE_WARNING_NAME = "warn-if"
  61. RULE_FAIL_FORMAT = "{0}: {1}"
  62. RULE_MSG_FAIL_FORMAT = "({0}) {1}: {2}"
  63. WARNING_RULE_FAILED = "warn because"
  64. def validate(self, config, meta_config):
  65. """Validate against any rules found in the meta_config."""
  66. self.reports = []
  67. rule_data = {self.RULE_ERROR_NAME: {}, self.RULE_WARNING_NAME: {}}
  68. evaluator = RuleEvaluator()
  69. for setting_id, sect_node in meta_config.value.items():
  70. for rule_opt in [self.RULE_ERROR_NAME, self.RULE_WARNING_NAME]:
  71. if rule_opt in sect_node.value:
  72. sect, opt = self._get_section_option_from_id(setting_id)
  73. node = config.get([sect, opt], no_ignore=True)
  74. if node is not None:
  75. rule = sect_node.get([rule_opt]).value
  76. id_rules = rule_data[rule_opt]
  77. id_rules.setdefault(setting_id, [])
  78. rule_lines = rule.splitlines()
  79. for rule_line in rule_lines:
  80. message = None
  81. if "#" in rule_line:
  82. rule_line, message = rule_line.split("#", 1)
  83. message = message.strip()
  84. rule_line = rule_line.strip()
  85. for rule_item in self.REC_RULE_SPLIT.split(rule_line):
  86. if not rule_item:
  87. continue
  88. id_rules[setting_id].append([rule_item, None])
  89. if id_rules[setting_id]:
  90. id_rules[setting_id][-1][-1] = message
  91. for rule_type in rule_data:
  92. is_warning = (rule_type == self.RULE_WARNING_NAME)
  93. if is_warning:
  94. f_type = self.WARNING_RULE_FAILED
  95. else:
  96. f_type = self.ERROR_RULE_FAILED
  97. for setting_id, rule_msg_list in rule_data[rule_type].items():
  98. section, option = self._get_section_option_from_id(setting_id)
  99. for (rule, message) in rule_msg_list:
  100. try:
  101. test_failed = evaluator.evaluate_rule(
  102. rule, setting_id, config)
  103. except RuleValueError as e:
  104. continue
  105. if test_failed:
  106. value = config.get([section, option]).value
  107. if message is None:
  108. info = self.RULE_FAIL_FORMAT.format(f_type, rule)
  109. else:
  110. info = self.RULE_MSG_FAIL_FORMAT.format(message,
  111. f_type,
  112. rule)
  113. self.add_report(section, option, value, info,
  114. is_warning)
  115. return self.reports
  116. class RuleEvaluator(rose.macro.MacroBase):
  117. """Evaluate logical expressions in the metadata."""
  118. ARRAY_EXPR = "{0}({1}) {2} {3}"
  119. ARRAY_FUNC_LOGIC = {"all": " and ",
  120. "any": " or "}
  121. INTERNAL_ID_SETTING = "_id{0}"
  122. INTERNAL_ID_VALUE = "_value{0}"
  123. INTERNAL_ID_THIS_SETTING = "_thisid{0}"
  124. INTERNAL_ID_SCI_NUM = "_scinum{0}"
  125. REC_ARRAY = {"all": re.compile(r"(\W)all\( *(\S+) *(\S+) *(.*?) *\)(\W)"),
  126. "any": re.compile(r"(\W)any\( *(\S+) *(\S+) *(.*?) *\)(\W)")}
  127. REC_CONFIG_ID = re.compile(r"""
  128. (?:\W|^) (?# Break or beginning)
  129. ( (?# Begin ID capture)
  130. [\w:]+ (?# 1st part of section, including :)
  131. (?:\{.*?\})? (?# Optional modifier for the section)
  132. (?:\([^)]*\))? (?# Optional element for the section)
  133. = (?# Section-option delimiter)
  134. [\w-]+ (?# Option name )
  135. (?:\(\d+\))? (?# Optional element for the option )
  136. ) (?# End ID capture )
  137. (?:\W|$) (?# Break or end)""", re.X)
  138. REC_LEN_FUNC = re.compile(r"(\W)len\( *(\S+) *\)(\W)")
  139. REC_SCI_NUM = re.compile(r"""
  140. (?:\W|^) (?# Break or beginning)
  141. ( (?# Begin number capture)
  142. [-+]? (?# Optional sign)
  143. [\d.]+ (?# Optional sign. Digit and dot)
  144. [ed][-+]? (?# Exponent, [edED] with ignored case)
  145. \d+ (?# Exponent number)
  146. ) (?# End number capture)
  147. (?:\W|$) (?# Break or end)
  148. """, re.I | re.X)
  149. REC_THIS_ELEMENT_ID = re.compile(r"""
  150. (?:\W|^) (?# Break or beginning)
  151. (this\(\d+\)) (?# 'this' element)
  152. (?:\W|$) (?# Break or end)""", re.X)
  153. REC_VALUE = re.compile(r'("[^"]*")')
  154. def evaluate_rule(self, rule, setting_id, config):
  155. rule_template_str, rule_id_values = self._process_rule(
  156. rule, setting_id, config)
  157. template = jinja2.Template(rule_template_str)
  158. return_string = template.render(rule_id_values)
  159. return ast.literal_eval(return_string)
  160. def _process_rule(self, rule, setting_id, config):
  161. if not (rule.startswith('{%') or rule.startswith('{-%')):
  162. rule = "{% if " + rule + " %}True{% else %}False{% endif %}"
  163. local_map = {"this": self._get_value_from_id(setting_id, config)}
  164. value_id_count = -1
  165. sci_num_count = -1
  166. for array_func_key, rec_regex in self.REC_ARRAY.items():
  167. for search_result in rec_regex.findall(rule):
  168. start, var_id, operator, value, end = search_result
  169. if var_id == "this":
  170. var_id = setting_id
  171. setting_value = self._get_value_from_id(var_id, config)
  172. array_value = rose.variable.array_split(str(setting_value))
  173. new_string = start + "("
  174. for elem_num in range(1, len(array_value) + 1):
  175. new_string += self.ARRAY_EXPR.format(var_id, elem_num,
  176. operator, value)
  177. if elem_num < len(array_value):
  178. new_string += self.ARRAY_FUNC_LOGIC[array_func_key]
  179. new_string += ")" + end
  180. rule = rec_regex.sub(new_string, rule, count=1)
  181. for search_result in self.REC_LEN_FUNC.findall(rule):
  182. start, var_id, end = search_result
  183. if var_id == "this":
  184. var_id = setting_id
  185. setting_value = self._get_value_from_id(var_id, config)
  186. array_value = rose.variable.array_split(str(setting_value))
  187. new_string = start + str(len(array_value)) + end
  188. rule = self.REC_LEN_FUNC.sub(new_string, rule, count=1)
  189. for search_result in self.REC_SCI_NUM.findall(rule):
  190. sci_num_count += 1
  191. key = self.INTERNAL_ID_SCI_NUM.format(sci_num_count)
  192. local_map[key] = self._evaluate(search_result)
  193. rule = rule.replace(search_result, key, 1)
  194. for search_result in self.REC_VALUE.findall(rule):
  195. value_string = search_result.strip('"')
  196. for key, value in local_map.items():
  197. if value == value_string:
  198. break
  199. else:
  200. value_id_count += 1
  201. key = self.INTERNAL_ID_VALUE.format(value_id_count)
  202. local_map[key] = value_string
  203. rule = rule.replace(search_result, key, 1)
  204. for search_result in self.REC_THIS_ELEMENT_ID.findall(rule):
  205. proper_id = search_result.replace("this", setting_id)
  206. value_string = self._get_value_from_id(proper_id, config)
  207. for key, value in local_map.items():
  208. if value == value_string:
  209. break
  210. else:
  211. x_id_num_str = search_result.replace("this", "").strip('()')
  212. key = self.INTERNAL_ID_THIS_SETTING.format(x_id_num_str)
  213. local_map[key] = value_string
  214. rule = rule.replace(search_result, key, 1)
  215. config_id_count = -1
  216. for search_result in self.REC_CONFIG_ID.findall(rule):
  217. value_string = self._get_value_from_id(search_result, config)
  218. for key, value in local_map.items():
  219. if value == value_string:
  220. break
  221. else:
  222. config_id_count += 1
  223. key = self.INTERNAL_ID_SETTING.format(config_id_count)
  224. local_map[key] = value_string
  225. rule = rule.replace(search_result, key, 1)
  226. return rule, local_map
  227. def _get_value_from_id(self, variable_id, config):
  228. section, option = self._get_section_option_from_id(variable_id)
  229. value = None
  230. opt_node = config.get([section, option], no_ignore=True)
  231. if opt_node is not None:
  232. value = opt_node.value
  233. if opt_node is None:
  234. if option is None:
  235. raise RuleValueError(variable_id)
  236. if (option.endswith(')') and '(' in option and
  237. option.count('(') == 1):
  238. option, element = option.rstrip(')').split('(')
  239. opt_node = config.get([section, option])
  240. if opt_node is not None:
  241. value = opt_node.value
  242. if value is not None:
  243. try:
  244. index = int(element)
  245. except (TypeError, ValueError) as e:
  246. raise RuleValueError(variable_id)
  247. val_array = rose.variable.array_split(value)
  248. try:
  249. return_value = val_array[index - 1]
  250. except IndexError as e:
  251. raise RuleValueError(variable_id)
  252. else:
  253. return self._evaluate(return_value)
  254. raise RuleValueError(variable_id)
  255. return self._evaluate(value)
  256. def _evaluate(self, string):
  257. try:
  258. return_value = float(string)
  259. except (TypeError, ValueError):
  260. return_value = string
  261. return return_value