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.

898 lines
40 KiB

  1. # vim: fileencoding=utf-8
  2. #
  3. import os
  4. import logging
  5. import importlib
  6. import importlib.util
  7. from jinja2 import Environment, PackageLoader
  8. from typing import Dict, Optional, Any, List, Generator
  9. from calculate.variables.datavars import (
  10. NamespaceNode,
  11. VariableNode,
  12. ListType,
  13. IntegerType,
  14. FloatType,
  15. IniType,
  16. TableType,
  17. Namespace,
  18. HashType,
  19. VariableNotFoundError,
  20. VariableError,
  21. )
  22. from calculate.utils.gentoo import ProfileWalker
  23. from calculate.utils.files import read_file, FilesError
  24. from calculate.utils.tools import Singleton
  25. from pyparsing import (
  26. ParseResults,
  27. Literal,
  28. Word,
  29. ZeroOrMore,
  30. Group,
  31. Optional as OptionalPattern,
  32. restOfLine,
  33. empty,
  34. printables,
  35. OneOrMore,
  36. lineno,
  37. line,
  38. SkipTo,
  39. LineEnd,
  40. Combine,
  41. nums,
  42. )
  43. from enum import Enum
  44. from contextlib import contextmanager
  45. class LoaderError(Exception):
  46. '''Исключение выбрасываемое загрузчиком, если тот в принципе не может
  47. загрузить переменные.'''
  48. pass
  49. class Define(Enum):
  50. assign = 0
  51. append = 1
  52. remove = 2
  53. class CalculateIniParser(metaclass=Singleton):
  54. '''Класс парсера calculate.ini файлов.'''
  55. def __init__(self):
  56. self.operations = {"=": Define.assign,
  57. "+=": Define.append,
  58. "-=": Define.remove}
  59. lbrack = Literal("[")
  60. rbrack = Literal("]")
  61. comment_symbol = Literal(';') | Literal('#')
  62. value_operation = (Literal("=") | Combine(Literal("+") + Literal("="))
  63. | Combine(Literal("-") + Literal("=")))
  64. comment = comment_symbol + OptionalPattern(restOfLine)
  65. section_name = (lbrack.suppress() + (~Word(nums)
  66. + Word(printables+'\t',
  67. excludeChars='[]'))
  68. + rbrack.suppress())
  69. value_name = Word(printables+'\t', excludeChars='=-+')
  70. clear_section = lbrack.suppress() + Group(empty) + rbrack.suppress()
  71. row_index = lbrack.suppress() + Word(nums) + rbrack.suppress()
  72. namespace_start = Group(OneOrMore(section_name)
  73. + (clear_section | ~lbrack)
  74. + LineEnd().suppress())
  75. table_start = Group(OneOrMore(section_name)
  76. + (row_index | clear_section | ~lbrack)
  77. + LineEnd().suppress())
  78. def add_lineno(string: str, location: int,
  79. tokens: ParseResults) -> None:
  80. tokens.append(lineno(location, string))
  81. section_start = (namespace_start('namespace') |
  82. table_start('table'))
  83. section_start.setParseAction(add_lineno)
  84. # Если содержимое ini-файла не предваряется заголовком секции,
  85. # значит эта строка ошибочна.
  86. unexpected = Group(~section_start + SkipTo(LineEnd(),
  87. include=True))("error")
  88. unexpected.setParseAction(self._unexpected_token)
  89. key_value = (~lbrack + value_name
  90. + value_operation + empty
  91. + restOfLine + LineEnd().suppress())
  92. def process_key_value(string: str, location: int,
  93. tokens: ParseResults) -> None:
  94. tokens[0] = tokens[0].strip()
  95. tokens[1] = tokens[1].strip()
  96. tokens.append(lineno(location, string))
  97. key_value.setParseAction(process_key_value)
  98. self.ini_section_parser = (section_start
  99. + Group(ZeroOrMore(
  100. Group(key_value
  101. | unexpected)))
  102. | unexpected)
  103. self.ini_section_parser.ignore(comment)
  104. def parse(self, data: str) -> Generator[Dict[str, tuple], None, None]:
  105. for tokens, start, end in self.ini_section_parser.scanString(data):
  106. if tokens.getName() == "error":
  107. if tokens[1].strip():
  108. yield({'error': (tokens[0], tokens[1])})
  109. continue
  110. section, section_lineno, defkeys = tokens
  111. if section.getName() == 'namespace':
  112. section_list = section.asList()
  113. if section_list[-1] == []:
  114. yield {'clear_section': (section_list[:-1],
  115. section_lineno)}
  116. else:
  117. yield {'start_section': (section_list, section_lineno)}
  118. for defkey in defkeys:
  119. if defkey.getName() == "error":
  120. if defkey[1].strip():
  121. yield({'error': (defkey[0], defkey[1])})
  122. continue
  123. yield {'define_key': (defkey[0], defkey[2],
  124. self.operations[defkey[1]],
  125. defkey[3])}
  126. else:
  127. table_list = section.asList()
  128. table_values = {}
  129. for defkey in defkeys:
  130. if defkey.getName() == "error":
  131. if defkey[1].strip():
  132. yield({'error': (defkey[0], defkey[1])})
  133. continue
  134. table_values.update({defkey[0]: defkey[2]})
  135. yield {'start_table': (table_list, table_values,
  136. section_lineno)}
  137. def _unexpected_token(self, string: str, location: int,
  138. tokens: ParseResults) -> list:
  139. '''Метод вызываемый парсером, если обнаружена некорректная строка,
  140. предназначен для получения некорректной строки и ее дальнейшего
  141. разбора.'''
  142. return [lineno(location, string), line(location, string)]
  143. class NamespaceIniFiller:
  144. '''Класс, предназначенный для наполнения Namespace объекта переменными
  145. из calculate.ini файла.'''
  146. available_sections = {'custom'}
  147. def __init__(self, restrict_creation: bool = True):
  148. self.ini_parser = CalculateIniParser()
  149. self._errors = []
  150. # Флаги, определяющие возможность создания новых переменных и новых
  151. # пространств имен в данном пространстве имен.
  152. self.restricted: bool = restrict_creation
  153. self.modify_only: bool = False
  154. def fill(self, namespace: NamespaceNode, ini_file_text: str) -> None:
  155. '''Метод для разбора calculate.ini файла и добавления всех его
  156. переменных в указанное пространство имен.'''
  157. self.namespace = namespace
  158. self.current_namespace = self.namespace
  159. self._errors = []
  160. for parsed_line in self.ini_parser.parse(ini_file_text):
  161. self._line_processor(**parsed_line)
  162. def _line_processor(self, start_section: Optional[tuple] = None,
  163. clear_section: Optional[tuple] = None,
  164. start_table: Optional[tuple] = None,
  165. define_key: Optional[tuple] = None,
  166. error: Optional[tuple] = None,
  167. **kwargs) -> None:
  168. '''Метод вызывающий обработку токенов, выдаваемых парсером в
  169. зависимости от их типа.'''
  170. if start_section is not None:
  171. self.start_section(*start_section)
  172. elif clear_section is not None:
  173. self.clear_section(*clear_section)
  174. elif start_table is not None:
  175. self.start_table(*start_table)
  176. elif define_key is not None:
  177. self.define_key(*define_key)
  178. elif error is not None:
  179. self._set_error(error[0], 'SyntaxError', error[1])
  180. def start_section(self, sections: str, lineno: int) -> None:
  181. '''Метод для получения доступа и создания пространств имен.'''
  182. if self.restricted:
  183. self.modify_only = sections[0] not in self.available_sections
  184. self.current_namespace = self.namespace
  185. for section in sections:
  186. if isinstance(self.current_namespace, Datavars):
  187. if section not in self.current_namespace:
  188. self._set_error(lineno, 'VariableError',
  189. "variables package '{}' is not found.".
  190. format(section))
  191. self.current_namespace = None
  192. return
  193. elif isinstance(self.current_namespace, NamespaceNode):
  194. if section not in self.current_namespace._namespaces:
  195. if (section in self.current_namespace._variables and
  196. self.current_namespace[section].variable_type
  197. is HashType):
  198. # Если секция является хэшем, используем ее.
  199. self.current_namespace = self.current_namespace.\
  200. _variables[section]
  201. return
  202. elif not self.modify_only:
  203. self.current_namespace.add_namespace(
  204. NamespaceNode(section))
  205. else:
  206. self._set_error(lineno, 'VariableError',
  207. "can not create namespace '{}.{}' in"
  208. " not 'custom' namespace.".format(
  209. self.current_namespace.get_fullname(),
  210. section))
  211. self.current_namespace = None
  212. return
  213. self.current_namespace = self.current_namespace.\
  214. _namespaces[section]
  215. def clear_section(self, sections: list, lineno: int) -> None:
  216. '''Метод для очистки пространства имен.'''
  217. if self.restricted:
  218. self.modify_only = sections[0] not in self.available_sections
  219. current_namespace = self.namespace
  220. for section in sections:
  221. if isinstance(current_namespace, Datavars):
  222. if section in current_namespace:
  223. current_namespace = current_namespace[section]
  224. else:
  225. self._set_error(lineno, 'VariableError',
  226. "can not clear"" namespace '{}'. Variables"
  227. " package '{}' is not found.".format(
  228. ".".join(sections),
  229. section))
  230. return
  231. elif isinstance(current_namespace, NamespaceNode):
  232. if section in current_namespace._namespaces:
  233. current_namespace = current_namespace[section]
  234. elif (section in current_namespace._variables and
  235. current_namespace._variables[section].variable_type
  236. is TableType):
  237. table_variable = current_namespace._variables[section]
  238. table_to_clear = table_variable.get_value()
  239. table_to_clear.clear()
  240. table_variable.source = table_to_clear
  241. return
  242. else:
  243. self._set_error(lineno, 'VariableError',
  244. "can not clear namespace '{}'. Namespace"
  245. " is not found.".format(
  246. ".".join(sections)))
  247. return
  248. if not self.modify_only:
  249. current_namespace.clear()
  250. else:
  251. self._set_error(lineno, 'VariableError',
  252. "can not clear namespace '{}' from not 'custom'"
  253. " namespace.".format(current_namespace.
  254. get_fullname()))
  255. def start_table(self, sections: str, row: dict, lineno: int) -> None:
  256. '''Метод для создания и модификации таблиц.'''
  257. if self.restricted:
  258. self.modify_only = sections[0] not in self.available_sections
  259. self.current_namespace = self.namespace
  260. row_index = int(sections.pop())
  261. table_name = sections.pop()
  262. for section in sections:
  263. if section not in self.current_namespace._namespaces:
  264. if not self.modify_only:
  265. self.current_namespace.add_namespace(
  266. NamespaceNode(section))
  267. else:
  268. self._set_error(lineno, 'VariableError',
  269. "can not create table '{}.{}', namespace"
  270. " '{}' is not found.".format(
  271. ".".join(sections),
  272. table_name, section))
  273. self.current_namespace = None
  274. return
  275. self.current_namespace = self.current_namespace.\
  276. _namespaces[section]
  277. if table_name not in self.current_namespace._variables:
  278. if not self.modify_only:
  279. table_variable = VariableNode(table_name,
  280. self.current_namespace,
  281. variable_type=TableType,
  282. source=[row])
  283. else:
  284. self._set_error(lineno, 'VariableError',
  285. "can not create table '{}.{}' in not 'custom'"
  286. " namespace.".format(self.current_namespace.
  287. get_fullname(),
  288. table_name))
  289. else:
  290. table_variable = self.current_namespace._variables[table_name]
  291. table = table_variable.get_value()
  292. if row_index < len(table):
  293. table.change_row(row, row_index)
  294. else:
  295. table.add_row(row)
  296. table_variable.source = table
  297. def define_key(self, key: str, value: str, optype: Define,
  298. lineno: int) -> None:
  299. '''Метод для создания и модификации переменных.'''
  300. if self.current_namespace is None:
  301. return
  302. if (isinstance(self.current_namespace, VariableNode) and
  303. self.current_namespace.variable_type is HashType):
  304. self.update_hash(key, value, optype, lineno)
  305. else:
  306. if optype == Define.assign:
  307. if key not in self.current_namespace:
  308. self.define_variable(key, value, lineno)
  309. else:
  310. self.change_value(key, value, lineno)
  311. elif optype == Define.append:
  312. if key not in self.current_namespace:
  313. self.define_variable(key, value, lineno)
  314. else:
  315. self.append_value(key, value, lineno)
  316. elif optype == Define.remove:
  317. if key not in self.current_namespace:
  318. self.define_variable(key, value, lineno)
  319. else:
  320. self.remove_value(key, value, lineno)
  321. def change_value(self, key: str, value: str, lineno: int) -> None:
  322. '''Метод для изменения значения переменной.'''
  323. variable = self.current_namespace[key]
  324. if variable.readonly:
  325. self._set_error(lineno, 'VariableError',
  326. "can not change readonly variable "
  327. f"'{self.current_namespace.get_fullname()}.{key}'")
  328. return
  329. variable.source = value
  330. def define_variable(self, key: str, value: str, lineno: int) -> None:
  331. '''Метод для создания переменных в calculate.ini файле.'''
  332. if not self.modify_only:
  333. VariableNode(key, self.current_namespace, variable_type=IniType,
  334. source=value)
  335. else:
  336. self._set_error(lineno, 'VariableError',
  337. "can not create variable '{}.{}' in not 'custom'"
  338. " namespace.".format(
  339. self.current_namespace.get_fullname(),
  340. key))
  341. def append_value(self, key: str, value: str, lineno: int) -> None:
  342. '''Метод выполняющий действия возложенные на оператор +=.'''
  343. variable = self.current_namespace[key]
  344. if variable.readonly:
  345. self._set_error(lineno, 'VariableError',
  346. "can not change readonly variable "
  347. f"'{self.current_namespace.get_fullname()}.{key}'")
  348. return
  349. variable_value = variable.get_value()
  350. if variable.variable_type is IniType:
  351. value_list = value.split(',')
  352. variable_list = variable_value.split(',')
  353. for item in value_list:
  354. if item not in variable_list:
  355. variable_list.append(item.strip())
  356. variable_value = ','.join(variable_list)
  357. elif variable.variable_type is ListType:
  358. value_list = value.split(',')
  359. for item in value_list:
  360. if item not in variable_value:
  361. variable_value.append(item.strip())
  362. elif variable.variable_type is IntegerType:
  363. variable_value += int(value)
  364. elif variable.variable_type is FloatType:
  365. variable_value += float(value)
  366. variable.source = variable_value
  367. def remove_value(self, key: str, value: str, lineno: int) -> None:
  368. '''Метод выполняющий действия возложенные на оператор -=.'''
  369. variable = self.current_namespace[key]
  370. if variable.readonly:
  371. self._set_error(lineno, 'VariableError',
  372. "can not change readonly variable "
  373. f"'{self.current_namespace.get_fullname()}.{key}'")
  374. return
  375. variable_value = variable.get_value()
  376. if variable.variable_type is IniType:
  377. value_list = value.split(',')
  378. variable_list = [item.strip() for item in
  379. variable_value.split(',')]
  380. for item in value_list:
  381. if item in variable_list:
  382. variable_list.remove(item.strip())
  383. variable_value = ','.join(variable_list)
  384. elif variable.variable_type is ListType:
  385. value_list = value.split(',')
  386. for item in value_list:
  387. if item in variable_value:
  388. variable_value.remove(item.strip())
  389. elif variable.variable_type is IntegerType:
  390. variable_value -= int(value)
  391. elif variable.variable_type is FloatType:
  392. variable_value -= float(value)
  393. variable.source = variable_value
  394. def update_hash(self, key: str, value: str, optype: Define,
  395. lineno: int) -> None:
  396. '''Метод для изменения переменных хэшей через calculate.ini.'''
  397. if self.current_namespace.readonly:
  398. self._set_error(lineno, 'VariableError',
  399. "can not change readonly hash variable "
  400. f"'{self.current_namespace.get_fullname()}'")
  401. return
  402. hash_to_update = self.current_namespace.get_value().get_hash()
  403. if key not in hash_to_update:
  404. # Если ключ отсутствует в хэше, то проверяем, является ли он
  405. # фиксированным.
  406. if self.current_namespace.fixed:
  407. self._set_error(lineno, 'VariableError',
  408. "key '{}' is unavailable for fixed hash '{}',"
  409. " available keys: '{}'.".format(
  410. key,
  411. self.current_namespace.get_fullname(),
  412. ', '.join(self.current_namespace.
  413. get_value()._fields)))
  414. return
  415. else:
  416. hash_to_update.update({key: value})
  417. elif optype == Define.assign:
  418. hash_to_update.update({key: value})
  419. elif optype == Define.append:
  420. current_value = hash_to_update[key]
  421. hash_to_update[key] = current_value + value
  422. elif optype == Define.remove:
  423. current_value = hash_to_update[key]
  424. if (isinstance(current_value, int) or
  425. isinstance(current_value, float)):
  426. hash_to_update[key] = current_value - value
  427. elif isinstance(current_value, str):
  428. value_list = [item.strip() for item in value.split(',')]
  429. current_value = [item.strip() for item in
  430. current_value.split(',')]
  431. for value_to_remove in value_list:
  432. if value_to_remove in current_value:
  433. current_value.remove(value_to_remove)
  434. hash_to_update[key] = ','.join(current_value)
  435. self.current_namespace.source = hash_to_update
  436. def _set_error(self, lineno: int, error_type: str, line: str) -> None:
  437. '''Метод для добавления ошибки в лог.'''
  438. self._errors.append("{}:{}: {}".format(error_type, lineno, line))
  439. @property
  440. def errors(self) -> List[str]:
  441. errors = self._errors
  442. self._errors = []
  443. return errors
  444. class VariableLoader:
  445. '''Класс загрузчика переменных из python-файлов и из ini-файлов.'''
  446. ini_basename = "calculate.ini"
  447. def __init__(self, datavars: "Datavars", variables_path: str,
  448. repository_map: Optional[Dict[str, str]] = None):
  449. self.datavars = datavars
  450. self.logger = datavars.logger
  451. self.ini_filler = NamespaceIniFiller()
  452. self.variables_path = os.path.join(
  453. __file__[:-len("calculate/variables/loader.py")],
  454. variables_path)
  455. self.variables_package = '.'.join(os.path.normpath(
  456. variables_path).split("/"))
  457. self.repository_map = repository_map
  458. def load_variables_package(self, package_name: str) -> None:
  459. '''Метод для загрузки пакетов с переменными.'''
  460. directory_path = os.path.join(self.variables_path, package_name)
  461. package = '{}.{}'.format(self.variables_package, package_name)
  462. package_namespace = NamespaceNode(package_name)
  463. self.datavars.root.add_namespace(package_namespace)
  464. self._fill_from_package(package_namespace, directory_path, package)
  465. def load_from_profile(self) -> None:
  466. '''Метод для загрузки переменных из calculate.ini профиля.'''
  467. # Проверяем наличие таблицы репозиториев в переменных.
  468. if self.repository_map == {}:
  469. return
  470. if self.repository_map is None:
  471. repositories_variable_path = ['os', 'gentoo', 'repositories']
  472. current_namespace = self.datavars
  473. for section in repositories_variable_path:
  474. if section in current_namespace:
  475. current_namespace = current_namespace[section]
  476. else:
  477. self.logger.error("Variable 'os.gentoo.repositories'"
  478. " is not found. Can not load profile"
  479. " variables.")
  480. return
  481. self.repository_map = self._get_repository_map(self.datavars)
  482. # Проверяем наличие пути к профилю в переменных.
  483. if ('profile' in self.datavars.os.gentoo and
  484. 'path' in self.datavars.os.gentoo.profile):
  485. profile_path = self.datavars.os.gentoo.profile.path
  486. else:
  487. self.logger.error("Variable 'os.gentoo.profile.path'"
  488. " is not found. Can not load profile"
  489. " variables.")
  490. return
  491. self.logger.info("Load variables from profile: '{}'.".format(
  492. profile_path))
  493. self._fill_from_profile_ini(profile_path)
  494. def load_user_variables(self) -> None:
  495. '''Метод для загрузки переменных из calculate.ini указанных в
  496. переменных env_order и env_path.'''
  497. try:
  498. env_order = self.datavars.main.cl.system.env_order
  499. env_path = self.datavars.main.cl.system.env_path
  500. except VariableNotFoundError as error:
  501. self.logger.warning("Can not load additional variables: {}".
  502. format(str(error)))
  503. return
  504. for ini_file in env_order:
  505. self.logger.info("Loading variables from file: '{}'".format(
  506. ini_file))
  507. if ini_file in env_path:
  508. self.fill_from_custom_ini(env_path[ini_file].value)
  509. self.logger.info("Variables from '{}' are loaded".format(
  510. ini_file))
  511. else:
  512. self.logger.warning("File '{}' is not found. Variables are"
  513. " not loaded".format(ini_file))
  514. def _fill_from_package(self, current_namespace: NamespaceNode,
  515. directory_path: str, package: str) -> None:
  516. '''Метод для зaполнения переменных из python-файла.'''
  517. file_nodes = []
  518. directory_nodes = []
  519. # Просматриваем директорию
  520. for node in os.scandir(directory_path):
  521. if node.is_dir():
  522. directory_nodes.append(node)
  523. elif node.is_file() and node.name.endswith('.py'):
  524. file_nodes.append(node)
  525. # Сначала загружаем переменные из файлов.
  526. for file_node in file_nodes:
  527. if not file_node.name.endswith('.py'):
  528. continue
  529. file_name = file_node.name[:-3]
  530. Namespace.set_current_namespace(current_namespace)
  531. # with self.test(file_name, current_namespace):
  532. # importlib.import_module('{}.{}'.format(package, file_name))
  533. spec = importlib.util.spec_from_file_location(
  534. '{}.{}'.format(package, file_name),
  535. file_node.path)
  536. module = importlib.util.module_from_spec(spec)
  537. spec.loader.exec_module(module)
  538. if hasattr(module, 'import_variables'):
  539. module.import_variables()
  540. # Обходим остальные директории.
  541. for directory_node in directory_nodes:
  542. namespace = NamespaceNode(directory_node.name)
  543. current_namespace.add_namespace(namespace)
  544. self._fill_from_package(namespace, directory_node.path,
  545. '{}.{}'.format(package,
  546. directory_node.name))
  547. def _fill_from_profile_ini(self, profile_path: str) -> None:
  548. '''Метод для зaполнения переменных из ini-файла.'''
  549. profile_walker = ProfileWalker(self.ini_basename,
  550. self.repository_map)
  551. for file_path in profile_walker.find(profile_path):
  552. try:
  553. ini_file_text = read_file(file_path)
  554. self.ini_filler.fill(self.datavars, ini_file_text)
  555. except FilesError:
  556. self.logger.error("Can not load profile variables from"
  557. " unexisting file: {}".format(file_path))
  558. def _get_repository_map(self, datavars: "Datavars") -> Dict[str, str]:
  559. '''Метод для получения из переменной словаря с репозиториями и путями
  560. к ним.'''
  561. return {repo['name']: repo['path']
  562. for repo in datavars.os.gentoo.repositories}
  563. def fill_from_custom_ini(self, file_path: str) -> None:
  564. '''Метод для заполнения переменных из конкретного указанного файла.'''
  565. if os.path.exists(file_path):
  566. ini_file_text = read_file(file_path)
  567. self.ini_filler.fill(self.datavars, ini_file_text)
  568. parsing_errors = self.ini_filler.errors
  569. if parsing_errors:
  570. for error in parsing_errors:
  571. self.logger.error(error)
  572. self.logger.warning('Some variables was not loaded.')
  573. else:
  574. self.logger.info('All variables are loaded.')
  575. else:
  576. self.logger.error("Variables are not loaded. File '{}' does"
  577. " not exist.".format(file_path))
  578. @contextmanager
  579. def test(self, file_name: str, namespace: NamespaceNode):
  580. '''Контекстный менеджер для тестирования.'''
  581. print('IMPORT: {}.{}'.format(namespace.get_fullname(), file_name))
  582. try:
  583. yield self
  584. finally:
  585. print('IMPORTED FROM: {}.{}'.format(namespace.get_fullname(),
  586. file_name))
  587. class CalculateIniSaver:
  588. '''Класс для сохранения значений переменных в указанные ini-файлы.'''
  589. def __init__(self):
  590. self.ini_parser = CalculateIniParser()
  591. self.operations = {Define.assign: '=',
  592. Define.append: '+=',
  593. Define.remove: '-='}
  594. file_loader = PackageLoader('calculate', 'variables')
  595. environment = Environment(loader=file_loader)
  596. self.ini_template = environment.get_template('ini_template')
  597. def save_to_ini(self, target_path: str,
  598. variables_to_save: Dict[str, Dict[str, Any]]) -> None:
  599. '''Метод для сохранения значений переменных, переданных в словаре, в
  600. указанный ini-файл.'''
  601. ini_file_text = read_file(target_path)
  602. ini_dictionary = self._parse_ini(ini_file_text)
  603. for namespace in variables_to_save:
  604. if namespace in ini_dictionary:
  605. ini_dictionary[namespace].update(variables_to_save[namespace])
  606. else:
  607. ini_dictionary[namespace] = variables_to_save[namespace]
  608. ini_file_text = self._get_ini_text(ini_dictionary)
  609. with open(target_path, 'w') as ini_file:
  610. ini_file.write(ini_file_text)
  611. def _parse_ini(self, ini_file_text: str) -> Dict[str, Dict[str, str]]:
  612. '''Метод для разбора текста ini-файла в словарь, в который далее будут
  613. добавляться измененные переменные.'''
  614. current_namespace = None
  615. ini_dictionary = dict()
  616. for parsed_line in self.ini_parser.parse(ini_file_text):
  617. line_type = next(iter(parsed_line))
  618. line_content = parsed_line[line_type]
  619. if (line_type == 'start_section' or
  620. line_type == 'start_table'):
  621. current_namespace = tuple(line_content[0])
  622. if current_namespace not in ini_dictionary:
  623. ini_dictionary[current_namespace] = dict()
  624. elif (line_type == 'clear_section' or
  625. line_type == 'clear_table'):
  626. current_namespace = (*line_content[0], '')
  627. ini_dictionary[current_namespace] = dict()
  628. elif line_type == 'define_key':
  629. namespace = ini_dictionary[current_namespace]
  630. namespace.update({line_content[0]:
  631. (self.operations[line_content[2]],
  632. line_content[1])})
  633. return ini_dictionary
  634. def _get_ini_text(self, ini_dictionary: Dict[str, Dict[str, Any]]) -> str:
  635. '''Метод для получения текста ini файла, полученного в результате
  636. наложения изменений из тегов save в шаблонах.'''
  637. ini_text = self.ini_template.render(ini_dictionary=ini_dictionary)
  638. return ini_text
  639. class Datavars:
  640. '''Класс для хранения переменных и управления ими.'''
  641. def __init__(self, variables_path: str = 'calculate/vars',
  642. repository_map: Optional[Dict[str, str]] = None,
  643. logger: Optional[logging.Logger] = None):
  644. self._variables_path: str = variables_path
  645. self._available_packages = self._get_available_packages()
  646. if logger is not None:
  647. self.logger = logger
  648. else:
  649. logger = logging.getLogger("main")
  650. # stream_handler = logging.StreamHandler()
  651. # logger.addHandler(stream_handler)
  652. self.logger = logger
  653. self.root = NamespaceNode('<root>')
  654. self._loader = VariableLoader(self, self._variables_path,
  655. repository_map=repository_map)
  656. Namespace.reset()
  657. Namespace.set_datavars(self)
  658. self._loader.load_from_profile()
  659. self._loader.load_user_variables()
  660. # Создаем словарь переменных, которые нужно сохранить потом в
  661. # ini-файлах.
  662. try:
  663. self.variables_to_save = {target: dict() for target in
  664. self.main.cl.system.env_order
  665. if target in
  666. self.main.cl.system.env_path}
  667. except VariableNotFoundError:
  668. self.variables_to_save = dict()
  669. def reset(self) -> None:
  670. '''Метод для сброса модуля переменных.'''
  671. self.root.clear()
  672. self.root = NamespaceNode('<root>')
  673. self._available_packages.clear()
  674. self._available_packages: Dict[str,
  675. str] = self._get_available_packages()
  676. Namespace.set_datavars(self)
  677. def _get_available_packages(self) -> Dict[str, str]:
  678. '''Метод для получения словаря с имеющимися пакетами переменных
  679. и путями к ним.'''
  680. variables_path = os.path.join(
  681. __file__[:-len("calculate/variables/loader.py")],
  682. self._variables_path)
  683. available_packages = dict()
  684. for file_name in os.listdir(variables_path):
  685. if file_name.startswith('__'):
  686. continue
  687. file_path = os.path.join(variables_path, file_name)
  688. if os.path.isdir(file_path):
  689. available_packages.update({file_name: file_path})
  690. return available_packages
  691. def _load_package(self, package_name: str) -> None:
  692. '''Метод для загрузки переменных содержащихся в указанном пакете.'''
  693. self.logger.info("Loading datavars package '{}'".format(package_name))
  694. try:
  695. self._loader.load_variables_package(package_name)
  696. except Exception as error:
  697. raise VariableError("Can not load datavars package: {}".
  698. format(error))
  699. @property
  700. def available_packages(self) -> set:
  701. packages = set(self._available_packages)
  702. packages.update({'custom'})
  703. return packages
  704. def __getattr__(self, package_name: str) -> NamespaceNode:
  705. '''Метод возвращает ноду пространства имен, соответствующего искомому
  706. пакету.'''
  707. if package_name in self.root._namespaces:
  708. return self.root[package_name]
  709. elif package_name == 'custom':
  710. custom_namespace = NamespaceNode('custom')
  711. self.root.add_namespace(custom_namespace)
  712. return self.root[package_name]
  713. elif package_name not in self._available_packages:
  714. raise VariableNotFoundError("datavars package '{}' is not found".
  715. format(package_name))
  716. else:
  717. self._load_package(package_name)
  718. return self.root[package_name]
  719. def __getitem__(self, package_name: str) -> NamespaceNode:
  720. '''Метод возвращает ноду пространства имен, соответствующего искомому
  721. пакету.'''
  722. if package_name in self.root:
  723. return self.root[package_name]
  724. elif package_name == 'custom':
  725. custom_namespace = NamespaceNode('custom')
  726. self.root.add_namespace(custom_namespace)
  727. return self.root[package_name]
  728. elif package_name == 'tasks':
  729. self.create_tasks()
  730. return self.root[package_name]
  731. elif package_name not in self._available_packages:
  732. raise VariableNotFoundError("variables package '{}' is not found".
  733. format(package_name))
  734. else:
  735. self._load_package(package_name)
  736. return self.root[package_name]
  737. def __contains__(self, package_name: str) -> bool:
  738. if package_name in self.root._namespaces:
  739. return True
  740. elif package_name == 'custom':
  741. custom_namespace = NamespaceNode('custom')
  742. self.root.add_namespace(custom_namespace)
  743. return True
  744. elif package_name == 'tasks':
  745. self.create_tasks()
  746. return True
  747. elif (package_name not in self._available_packages
  748. and package_name != 'custom'):
  749. return False
  750. else:
  751. self._load_package(package_name)
  752. return True
  753. def add_namespace(self, namespace_node: NamespaceNode) -> None:
  754. self.root.add_namespace(namespace_node)
  755. def create_tasks(self) -> None:
  756. '''Метод для создания всех необходимых пространств имен для работы
  757. задач.'''
  758. tasks = NamespaceNode('tasks')
  759. self.add_namespace(tasks)
  760. env = NamespaceNode('env')
  761. tasks.add_namespace(env)
  762. env.add_namespace('loop')
  763. @property
  764. def _namespaces(self) -> NamespaceNode:
  765. return self.root._namespaces
  766. def save_variables(self) -> None:
  767. '''Метод для сохранения значений переменных в calculate.ini файлах.'''
  768. target_paths = self.main.cl.system.env_path
  769. saver = CalculateIniSaver()
  770. for target in self.variables_to_save:
  771. if self.variables_to_save[target]:
  772. dict_to_save = self.variables_to_save[target]
  773. target_path = target_paths[target].value
  774. saver.save_to_ini(target_path, dict_to_save)
  775. self.logger.info("Variables for '{}' is saved in the"
  776. " file: {}".format(target, target_path))