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.

1817 lines
80 KiB

  1. # vim: fileencoding=utf-8
  2. #
  3. from jinja2.ext import Extension
  4. from jinja2.lexer import Token
  5. from jinja2.parser import Parser
  6. from jinja2 import (
  7. nodes,
  8. Template,
  9. Environment,
  10. contextfunction,
  11. FileSystemLoader,
  12. TemplateSyntaxError,
  13. )
  14. from jinja2.utils import missing
  15. from jinja2.runtime import Context, Undefined
  16. from collections.abc import MutableMapping
  17. from collections import OrderedDict
  18. from importlib import import_module
  19. from pprint import pprint
  20. import copy
  21. import re
  22. import os
  23. import stat
  24. from typing import (
  25. Optional,
  26. Union,
  27. List,
  28. Any,
  29. Dict,
  30. Tuple,
  31. Iterator,
  32. )
  33. from ..utils.package import (
  34. PackageAtomParser,
  35. PackageAtomError,
  36. PackageAtomName,
  37. NOTEXIST,
  38. Package,
  39. Version,
  40. )
  41. from ..utils.files import (
  42. check_directory_link,
  43. check_command,
  44. join_paths,
  45. FilesError,
  46. )
  47. from calculate.variables.datavars import (
  48. VariableNotFoundError,
  49. NamespaceNode,
  50. VariableNode,
  51. IntegerType,
  52. FloatType,
  53. ListType,
  54. HashType,
  55. IniType,
  56. )
  57. from calculate.utils.fs import readFile
  58. from calculate.variables.loader import Datavars
  59. import calculate.templates.template_filters as template_filters
  60. # Типы шаблона: директория или файл.
  61. DIR, FILE, LINK = range(3)
  62. # Словарь, в котором можно регистрировать фильтры.
  63. CALCULATE_FILTERS = {"cut": template_filters.cut}
  64. class IncorrectParameter(Exception):
  65. pass
  66. class DefaultParameterError(Exception):
  67. pass
  68. class SaveError(Exception):
  69. pass
  70. class ConditionFailed(TemplateSyntaxError):
  71. pass
  72. class Variables(MutableMapping):
  73. '''Класс-заглушка вместо модуля переменных для тестов.'''
  74. def __init__(self, *args, **kwargs):
  75. self.__attrs: dict = dict(*args, **kwargs)
  76. self.__iter: Union[Iterator, None] = None
  77. def __next__(self) -> Any:
  78. if self._iter is None:
  79. self._iter = iter(self.__attrs)
  80. return next(self._iter)
  81. def __getattribute__(self, name: str) -> Any:
  82. if name == '_Variables__attrs':
  83. return super().__getattribute__(name)
  84. if name == 'available_packages':
  85. return super().__getattribute__(name)
  86. if name == '_variables':
  87. return self.__attrs
  88. if name == '_iter':
  89. return self._iter
  90. try:
  91. return self.__attrs[name]
  92. except KeyError:
  93. raise AttributeError(name)
  94. @property
  95. def available_packages(self) -> set:
  96. packages = set(self.__attrs.keys())
  97. packages.update({'custom'})
  98. return packages
  99. def __getitem__(self, name: str) -> Any:
  100. return self.__attrs[name]
  101. def __setitem__(self, name: str, value: Any) -> None:
  102. self.__attrs[name] = value
  103. def __delitem__(self, name: str) -> None:
  104. del self.__attrs[name]
  105. def __iter__(self) -> Iterator:
  106. return iter(self.__attrs)
  107. def __len__(self) -> int:
  108. return len(self.__attrs)
  109. def __repr__(self) -> str:
  110. return '<Variables {}>'.format(self.__attrs)
  111. def __contains__(self, name: str) -> bool:
  112. return name in self.__attrs
  113. def __hash__(self) -> int:
  114. return hash(id(self))
  115. class ParametersProcessor:
  116. '''Класс для проверки и разбора параметров шаблона.'''
  117. available_parameters: set = {'name', 'path', 'append', 'chmod', 'chown',
  118. 'autoupdate', 'env', 'force', 'source',
  119. 'format', 'unbound', 'mirror', 'run', 'exec',
  120. 'env', 'package', 'merge', 'postmerge',
  121. 'action', 'rebuild', 'restart', 'stop',
  122. 'start', 'handler', 'notify', 'group',
  123. 'convert', 'stretch', 'execsql'}
  124. inheritable_parameters: set = {'chmod', 'chown', 'autoupdate', 'env',
  125. 'package', 'action', 'handler', 'group'}
  126. # Параметры по умолчанию для файлов --
  127. # будут заполняться из __datavars__
  128. file_default_parameters: dict = {}
  129. # Параметры по умолчанию для директорий --
  130. # будут заполняться из __datavars__
  131. directory_default_parameters: dict = {}
  132. available_appends: set = set()
  133. available_formats: dict = dict()
  134. format_is_inspected: bool = False
  135. chmod_value_regular = re.compile(
  136. r'([r-][w-][Xx-])([r-][w-][Xx-])([r-][w-][Xx-])')
  137. def __init__(self,
  138. parameters_container: Optional["ParametersContainer"] = None,
  139. chroot_path: str = '/',
  140. datavars_module: Union[Datavars,
  141. NamespaceNode,
  142. Variables] = Variables(),
  143. for_package: Optional[Package] = None):
  144. self.chroot_path: str = chroot_path
  145. self.template_type: int = DIR
  146. self.datavars_module: Union[Datavars,
  147. NamespaceNode,
  148. Variables] = datavars_module
  149. self._parameters_container: ParametersContainer = parameters_container
  150. self.package_atom_parser: PackageAtomParser = PackageAtomParser(
  151. chroot_path=chroot_path)
  152. self._groups: dict = {}
  153. try:
  154. groups = list(datavars_module.main.cl.groups._variables.keys())
  155. for group in groups:
  156. if isinstance(datavars_module, (Datavars, NamespaceNode)):
  157. packages = datavars_module.main.cl.groups[group].get_value(
  158. ).get_table()
  159. else:
  160. packages = datavars_module.main.cl.groups[group]
  161. self._groups.update({group: packages})
  162. except Exception:
  163. pass
  164. self._inspect_formats_package()
  165. self._for_package: Union[Package, None] = for_package
  166. # Если добавляемый параметр нуждается в проверке -- добавляем сюда
  167. # метод для проверки.
  168. self.checkers_list = OrderedDict({
  169. 'package': self.check_package_parameter,
  170. 'group': self.check_group_parameter,
  171. 'append': self.check_append_parameter,
  172. 'rebuild': self.check_rebuild_parameter,
  173. 'restart': self.check_restart_parameter,
  174. 'run': self.check_run_parameter,
  175. 'exec': self.check_exec_parameter,
  176. 'stop': self.check_stop_parameter,
  177. 'start': self.check_start_parameter,
  178. 'chown': self.check_chown_parameter,
  179. 'chmod': self.check_chmod_parameter,
  180. 'autoupdate': self.check_autoupdate_parameter,
  181. 'source': self.check_source_parameter,
  182. 'force': self.check_force_parameter,
  183. 'env': self.check_env_parameter,
  184. 'merge': self.check_merge_parameter,
  185. 'format': self.check_format_parameter,
  186. 'handler': self.check_handler_parameter,
  187. 'notify': self.check_notify_parameter,
  188. 'convert': self.check_convert_parameter,
  189. 'stretch': self.check_stretch_parameter,
  190. 'execsql': self.check_execsql_parameter,
  191. })
  192. # Если добавляемый параметр должен быть проверен после того, как
  193. # будет закончен парсинг всех других параметров -- добавляем сюда метод
  194. # для проверки.
  195. self.postparse_checkers_list = OrderedDict({
  196. 'package': self.check_postparse_package,
  197. 'append': self.check_postparse_append,
  198. 'source': self.check_postparse_source,
  199. 'autoupdate': self.check_postparse_autoupdate,
  200. 'run': self.check_postparse_run,
  201. 'exec': self.check_postparse_exec,
  202. 'handler': self.check_postparse_handler,
  203. 'convert': self.check_postparse_convert,
  204. 'stretch': self.check_postparse_stretch,
  205. 'execsql': self.check_postparse_execsql,
  206. })
  207. # Если параметр является наследуемым только при некоторых условиях --
  208. # указываем здесь эти условия.
  209. self.inherit_conditions = {'chmod': self.is_chmod_inheritable}
  210. def set_parameters_container(self,
  211. parameters_container: "ParametersContainer"
  212. ) -> None:
  213. '''Метод для установки текущего контейнера параметров.'''
  214. self._parameters_container = parameters_container
  215. self._added_parameters = set()
  216. @property
  217. def for_package(self) -> Union[Package, None]:
  218. return self._for_package
  219. @for_package.setter
  220. def for_package(self, package: Package) -> None:
  221. self._for_package = package
  222. def __getattr__(self, parameter_name: str) -> Any:
  223. if parameter_name not in self.available_parameters:
  224. raise IncorrectParameter("Unknown parameter: '{}'".
  225. format(parameter_name))
  226. elif parameter_name not in self._parameters_container:
  227. return False
  228. else:
  229. return self._parameters_container[parameter_name]
  230. def check_template_parameter(self, parameter_name: str,
  231. parameter_value: Any,
  232. template_type: int, lineno: int) -> None:
  233. '''Метод, проверяющий указанный параметр.'''
  234. self.lineno = lineno
  235. self.template_type = template_type
  236. if parameter_name not in self.available_parameters:
  237. raise IncorrectParameter("Unknown parameter '{0}'".
  238. format(parameter_name))
  239. elif parameter_name in self.checkers_list:
  240. checked_value = self.checkers_list[parameter_name](parameter_value)
  241. else:
  242. checked_value = parameter_value
  243. # Способ пропустить параметр, если он корректен, но добавлять не нужно
  244. if checked_value is None:
  245. return
  246. self._added_parameters.add(parameter_name)
  247. if (parameter_name in self.inheritable_parameters and
  248. self.template_type == DIR):
  249. if parameter_name in self.inherit_conditions:
  250. if self.inherit_conditions[parameter_name](
  251. parameter_value):
  252. self._parameters_container.set_inheritable(
  253. {parameter_name: checked_value})
  254. return
  255. else:
  256. self._parameters_container.set_inheritable(
  257. {parameter_name: checked_value})
  258. return
  259. self._parameters_container.set_parameter({parameter_name:
  260. checked_value})
  261. def check_postparse_parameters(self) -> None:
  262. '''Метод, запускающий проверку параметров после их разбора.'''
  263. for parameter, parameter_checker in\
  264. self.postparse_checkers_list.items():
  265. if parameter not in self._added_parameters:
  266. continue
  267. parameter_value = self._parameters_container[parameter]
  268. result = parameter_checker(parameter_value)
  269. if result is not None:
  270. self._parameters_container.change_parameter(parameter,
  271. result)
  272. def check_template_parameters(self, parameters: dict,
  273. template_type: int, lineno: int) -> None:
  274. '''Метод, запускающий проверку указанных параметров.'''
  275. self.template_type = template_type
  276. self.lineno = lineno
  277. for parameter_name in parameters:
  278. if parameter_name not in self.available_parameters:
  279. raise IncorrectParameter("Unknown parameter '{0}'".
  280. format(parameter_name))
  281. elif parameter_name in self.checkers_list:
  282. checked_value = self.checkers_list[parameter_name](
  283. parameters[parameter_name]
  284. )
  285. else:
  286. checked_value = parameters[parameter_name]
  287. if (template_type == DIR and
  288. parameter_name in self.inheritable_parameters):
  289. if parameter_name in self.inherit_conditions:
  290. if self.inherit_conditions[parameter_name](
  291. parameters[parameter_name]):
  292. self._parameters_container.set_inheritable(
  293. parameter_name=checked_value)
  294. continue
  295. else:
  296. self._parameters_container.set_inheritable(
  297. parameter_name=checked_value)
  298. continue
  299. self._parameters_container.set_parameter(
  300. parameter_name=checked_value)
  301. # Методы для проверки параметров во время разбора шаблона.
  302. def check_package_parameter(self, parameter_value: Any) -> str:
  303. if not isinstance(parameter_value, str):
  304. raise IncorrectParameter("'package' parameter must have value of"
  305. " the 'str' type, not"
  306. f" {type(parameter_value)}")
  307. return parameter_value
  308. def check_group_parameter(self, parameter_value: Any) -> List[str]:
  309. if isinstance(parameter_value, str):
  310. result = [group.strip() for group in parameter_value.split(',')]
  311. elif isinstance(parameter_value, (list, tuple)):
  312. result = parameter_value if isinstance(parameter_value, list) else\
  313. list(parameter_value)
  314. else:
  315. raise IncorrectParameter("'package' parameter must have value of"
  316. " the 'str', 'list' or 'tuple' type, not"
  317. f" {type(parameter_value)}")
  318. for group in result:
  319. if group != "install" and group not in self._groups:
  320. raise IncorrectParameter(f"'group' parameter value '{group}'"
  321. " is not available. Available values:"
  322. f" {', '.join(self._groups.keys())}")
  323. return result
  324. def check_append_parameter(self, parameter_value: Any) -> str:
  325. if parameter_value not in self.available_appends:
  326. raise IncorrectParameter("Unacceptable value '{}' of parameter"
  327. " 'append'".format(parameter_value))
  328. return parameter_value
  329. def check_merge_parameter(self, parameter_value: Any
  330. ) -> List[PackageAtomName]:
  331. packages_list = []
  332. packages_names = parameter_value.split(',')
  333. for package_name in packages_names:
  334. package_name = package_name.strip()
  335. try:
  336. atom_object = self.package_atom_parser.\
  337. parse_package_parameter(package_name)
  338. packages_list.append(atom_object)
  339. except PackageAtomError:
  340. continue
  341. return packages_list
  342. def check_rebuild_parameter(self, parameter_value: Any) -> bool:
  343. if not isinstance(parameter_value, bool):
  344. raise IncorrectParameter("'rebuild' parameter value is not bool")
  345. elif 'package' not in self._parameters_container:
  346. raise IncorrectParameter(("'rebuild' parameter is set without "
  347. "'package' parameter"))
  348. return parameter_value
  349. def check_restart_parameter(self, parameter_value: Any) -> str:
  350. if parameter_value and isinstance(parameter_value, str):
  351. return parameter_value
  352. else:
  353. raise IncorrectParameter(
  354. "'restart' parameter value is not correct")
  355. def check_stop_parameter(self, parameter_value: Any) -> str:
  356. if not parameter_value and isinstance(parameter_value, bool):
  357. raise IncorrectParameter("'stop' parameter value is empty")
  358. return parameter_value
  359. def check_start_parameter(self, parameter_value: Any) -> str:
  360. if not parameter_value and isinstance(parameter_value, bool):
  361. raise IncorrectParameter("'start' parameter value is empty")
  362. return parameter_value
  363. def check_run_parameter(self, parameter_value: Any) -> str:
  364. if self.template_type == DIR:
  365. raise IncorrectParameter("'run' parameter is not available in"
  366. " directory templates")
  367. if not parameter_value and isinstance(parameter_value, bool):
  368. raise IncorrectParameter("'run' parameter value is empty")
  369. try:
  370. interpreter_path = check_command(parameter_value)
  371. except FilesError:
  372. raise IncorrectParameter("interpreter from 'run' parameter not"
  373. " found")
  374. return interpreter_path
  375. def check_exec_parameter(self, parameter_value: Any) -> str:
  376. if self.template_type == DIR:
  377. raise IncorrectParameter("'exec' parameter is not available in"
  378. " directory templates")
  379. if not parameter_value and isinstance(parameter_value, bool):
  380. raise IncorrectParameter("'exec' parameter value is empty")
  381. try:
  382. interpreter_path = check_command(parameter_value)
  383. except FilesError:
  384. raise IncorrectParameter("interpreter from 'exec' parameter is not"
  385. " found")
  386. return interpreter_path
  387. def check_chown_parameter(self, parameter_value: Any) -> dict:
  388. if not parameter_value or isinstance(parameter_value, bool):
  389. raise IncorrectParameter("'chown' parameter value is empty.")
  390. parameter_value = self.get_chown_values(parameter_value)
  391. return parameter_value
  392. def check_chmod_parameter(self, parameter_value: Any) -> Union[int, tuple]:
  393. result = self.chmod_value_regular.search(parameter_value)
  394. if result:
  395. return self._translate_symbol_chmod(result)
  396. elif parameter_value.isdigit():
  397. parameter_value = int(parameter_value, 8)
  398. return parameter_value
  399. else:
  400. raise IncorrectParameter("'chmod' parameter value is not correct")
  401. def _translate_symbol_chmod(self, result) -> Tuple[int, int]:
  402. '''Метод для перевода буквенного значения chmod в числовое.
  403. Возвращает кортеж (chmod, x_mask):
  404. chmod -- число, полученное из последовательности битов, где
  405. "r", "w" и "x" -> 1, "-" и "X" -> 0;
  406. x_mask -- маска, полученная из последовательности битов, где
  407. "X" -> 1, "r", "w", "-" и "x" -> 0. Она необходима для получения
  408. значения chmod для файлов.'''
  409. chmod = ''
  410. x_mask = ''
  411. for group_index in range(3):
  412. group = result.groups()[group_index]
  413. for sym_index in range(3):
  414. if group[sym_index] in {'-', 'X'}:
  415. chmod = chmod + '0'
  416. else:
  417. chmod = chmod + '1'
  418. if group[sym_index] == 'X':
  419. x_mask = x_mask + "1"
  420. else:
  421. x_mask = x_mask + "0"
  422. return (int(chmod, 2), int(x_mask, 2))
  423. def check_source_parameter(self, parameter_value: Any
  424. ) -> Union[str, Tuple[bool, str]]:
  425. if not parameter_value or isinstance(parameter_value, bool):
  426. raise IncorrectParameter("'source' parameter value is empty")
  427. if self.chroot_path != '/':
  428. real_path = join_paths(self.chroot_path, parameter_value)
  429. else:
  430. real_path = parameter_value
  431. # Ставим True, чтобы потом проверить этот параметр в postparse
  432. if not os.path.exists(real_path):
  433. return (False, real_path)
  434. source_file_type = DIR if os.path.isdir(real_path) else FILE
  435. # Проверяем, совпадают ли типы шаблона и файла, указанного в source
  436. if (self.template_type != source_file_type):
  437. raise IncorrectParameter(
  438. "the type of the 'source' file does not match"
  439. " the type of the template file")
  440. # Проверяем, не является ли файл из source зацикленной ссылкой.
  441. if (source_file_type == DIR and os.path.islink(real_path)):
  442. try:
  443. check_directory_link(real_path, chroot_path=self.chroot_path)
  444. except FilesError as error:
  445. raise IncorrectParameter(
  446. "the link from 'source' is not correct: {}".
  447. format(str(error)))
  448. return os.path.normpath(real_path)
  449. def check_env_parameter(self, parameter_value: Any) -> Union[None, set]:
  450. env_set = set()
  451. for env_value in parameter_value.split(','):
  452. env_value = env_value.strip()
  453. name_parts = env_value.split('.')
  454. namespace = self.datavars_module
  455. for name in name_parts:
  456. if name not in namespace:
  457. raise ConditionFailed(
  458. (f"Namespace '{env_value}' from 'env' parameter"
  459. " does not exist."),
  460. self.lineno)
  461. namespace = namespace[name]
  462. env_set.add(namespace)
  463. CalculateContext._env_set.add(namespace)
  464. # Если шаблон файла -- не добавляем env в контейнер,
  465. # а только используем для рендеринга шаблона.
  466. if self.template_type is None:
  467. return None
  468. if self._parameters_container.env:
  469. env_set.union(self._parameters_container.env)
  470. return env_set
  471. def check_force_parameter(self, parameter_value: Any) -> bool:
  472. if isinstance(parameter_value, bool):
  473. return parameter_value
  474. else:
  475. raise IncorrectParameter("'force' parameter value is not bool")
  476. def check_autoupdate_parameter(self, parameter_value: Any) -> bool:
  477. if isinstance(parameter_value, bool):
  478. return parameter_value
  479. else:
  480. raise IncorrectParameter(
  481. "'autoupdate' parameter value is not bool.")
  482. def check_format_parameter(self, parameter_value: Any) -> str:
  483. if self.template_type == DIR:
  484. raise IncorrectParameter("'format' parameter is redundant for"
  485. " directory templates.")
  486. if isinstance(parameter_value, str):
  487. if parameter_value not in self.available_formats:
  488. raise IncorrectParameter(f"'{parameter_value}' value of the"
  489. " 'format' parameter is not"
  490. " available.")
  491. return parameter_value
  492. raise IncorrectParameter("'format' parameter must be string value not"
  493. f" {type(parameter_value)}.")
  494. def check_handler_parameter(self, parameter_value: Any) -> str:
  495. if not isinstance(parameter_value, str):
  496. raise IncorrectParameter("'handler' parameter must be string"
  497. f" value not {type(parameter_value)}.")
  498. return parameter_value
  499. def check_notify_parameter(self, parameter_value: Any) -> List[str]:
  500. if isinstance(parameter_value, list):
  501. return parameter_value
  502. elif isinstance(parameter_value, str):
  503. return [parameter.strip() for parameter in
  504. parameter_value.split(',')]
  505. raise IncorrectParameter("'notify' parameter must be string or list"
  506. f" value not {type(parameter_value)}.")
  507. def check_convert_parameter(self, parameter_value: Any) -> str:
  508. if not isinstance(parameter_value, str):
  509. raise IncorrectParameter("'convert' parameter value must be string"
  510. f" not '{type(parameter_value)}'.")
  511. parameter_value = parameter_value.strip().upper()
  512. try:
  513. available_image_formats =\
  514. self.datavars_module.main.cl_image_formats
  515. except VariableNotFoundError:
  516. # TODO возможно стоит кидать ошибку.
  517. available_image_formats = ["JPEG", "PNG", "GIF", "JPG"]
  518. if parameter_value not in available_image_formats:
  519. raise IncorrectParameter(f"'{parameter_value}' image format is "
  520. "not available. Available image formats: "
  521. f"'{', '.join(available_image_formats)}.'"
  522. )
  523. return parameter_value
  524. def check_stretch_parameter(self, parameter_value: Any) -> bool:
  525. if not isinstance(parameter_value, bool):
  526. raise IncorrectParameter("'stretch' parameter value should be bool"
  527. f" value not '{type(parameter_value)}'")
  528. return parameter_value
  529. def check_execsql_parameter(self, parameter_value: Any) -> bool:
  530. if not isinstance(parameter_value, str):
  531. raise IncorrectParameter("'execsql' parameter value should be str"
  532. f" value not '{type(parameter_value)}'")
  533. available_values = {"rollback", "stop", "continue"}
  534. parameter_value = parameter_value.lower().strip()
  535. if parameter_value not in available_values:
  536. raise IncorrectParameter("'exesql' parameter value"
  537. f" '{parameter_value}' is not available."
  538. " Available values: "
  539. f"{', '.join(available_values)}")
  540. return parameter_value
  541. # Методы для проверки параметров после разбора всего шаблона.
  542. def check_postparse_append(self, parameter_value: str) -> None:
  543. if parameter_value == 'link':
  544. if 'source' not in self._parameters_container:
  545. raise IncorrectParameter("append = 'link' without source "
  546. "parameter.")
  547. if self._parameters_container.run:
  548. raise IncorrectParameter("'append' parameter is not 'compatible' "
  549. "with the 'run' parameter")
  550. if self._parameters_container.exec:
  551. raise IncorrectParameter("'append' parameter is not 'compatible' "
  552. "with the 'exec' parameter")
  553. def check_postparse_run(self, parameter_value: str) -> None:
  554. if self._parameters_container.append:
  555. raise IncorrectParameter("'run' parameter is not 'compatible' "
  556. "with the 'append' parameter")
  557. if self._parameters_container.exec:
  558. raise IncorrectParameter("'run' parameter is not 'compatible' "
  559. "with the 'exec' parameter")
  560. def check_postparse_exec(self, parameter_value: str) -> None:
  561. if self._parameters_container.append:
  562. raise IncorrectParameter("'exec' parameter is not 'compatible' "
  563. "with the 'append' parameter")
  564. if self._parameters_container.run:
  565. raise IncorrectParameter("'exec' parameter is not 'compatible' "
  566. "with the 'run' parameter")
  567. def check_postparse_source(self,
  568. parameter_value: Union[str, Tuple[bool, str]]
  569. ) -> None:
  570. # Если файл по пути source не существует, но присутствует параметр
  571. # mirror -- пропускаем шаблон для того, чтобы целевой файл мог быть
  572. # удален в исполнительном модуле.
  573. if isinstance(parameter_value, tuple):
  574. if ((self._parameters_container.append == "link" and
  575. self._parameters_container.force)
  576. or self._parameters_container.format == "backgrounds"):
  577. self._parameters_container['source'] = parameter_value[1]
  578. elif not self._parameters_container.mirror:
  579. raise IncorrectParameter(
  580. "File from 'source' parameter does not exist")
  581. elif (self.template_type == DIR and
  582. ('append' not in self._parameters_container or
  583. self._parameters_container['append'] != 'link')):
  584. raise IncorrectParameter(
  585. ("'source' parameter is set without "
  586. "append = 'link' for directory template")
  587. )
  588. def check_postparse_autoupdate(self, parameter_value: bool) -> None:
  589. if self._parameters_container.unbound:
  590. raise IncorrectParameter("'unbound' parameter is incompatible"
  591. " with 'autoupdate' parameter")
  592. def check_postparse_handler(self, parameter_value: bool) -> None:
  593. if self._parameters_container.merge:
  594. raise IncorrectParameter("'merge' parameter is not available"
  595. " in handler templates")
  596. elif (self._parameters_container.package and
  597. not self._parameters_container.is_inherited('package')):
  598. raise IncorrectParameter("'package' parameter is not available"
  599. " in handler templates")
  600. def check_postparse_package(self, parameter_value: str) -> None:
  601. groups = []
  602. package_atom = PackageAtomParser.parse_atom_name(parameter_value)
  603. if (self._parameters_container is None
  604. or not self._parameters_container.group):
  605. # Если параметр group не задан или метод используется для проверки
  606. # отдельного параметра package -- делаем только проверку install.
  607. # Предполагающую проверку существования пакета.
  608. groups.append('install')
  609. else:
  610. groups = self._parameters_container.group
  611. for group in groups:
  612. if group == 'install':
  613. try:
  614. result = self.package_atom_parser.parse_package_parameter(
  615. package_atom)
  616. return result
  617. except PackageAtomError as error:
  618. if error.errno != NOTEXIST:
  619. raise IncorrectParameter(error.message)
  620. elif self._check_package_group(package_atom,
  621. self._groups[group]):
  622. if (self._parameters_container is not None
  623. and self._parameters_container.package):
  624. self._parameters_container.remove_parameter('package')
  625. return
  626. raise ConditionFailed(f"package '{parameter_value}'"
  627. " does not match the template condition",
  628. self.lineno if hasattr(self, 'lineno') else 0)
  629. def _check_package_group(self, package: dict, group_packages: list
  630. ) -> bool:
  631. '''Метод для проверки соответствия описания пакета, заданного словарем,
  632. какому-либо описанию пакета, заданного в переменных groups.'''
  633. for group_package in group_packages:
  634. for parameter in ['category', 'name', 'version', 'slot']:
  635. if package[parameter] is not None:
  636. if (group_package[parameter] is None
  637. or group_package[parameter] != package[parameter]):
  638. break
  639. else:
  640. if package['use_flags'] is not None:
  641. if group_package['use_flags'] is None:
  642. continue
  643. else:
  644. for use_flag in package['use_flags']:
  645. if use_flag not in group_package['use_flags']:
  646. continue
  647. return True
  648. return False
  649. def check_postparse_convert(self, parameter_value: str) -> None:
  650. template_format = self._parameters_container.format
  651. if not template_format or template_format != "backgrounds":
  652. raise IncorrectParameter("'convert' parameter available for"
  653. " 'backgrounds' format only.")
  654. def check_postparse_stretch(self, parameter_value: str) -> None:
  655. template_format = self._parameters_container.format
  656. if not template_format or template_format != "backgrounds":
  657. raise IncorrectParameter("'stretch' parameter available for"
  658. " 'backgrounds' format only.")
  659. def check_postparse_execsql(self, parameter_value: str) -> None:
  660. template_format = self._parameters_container.format
  661. if not template_format or template_format != "sqlite":
  662. raise IncorrectParameter("'execsql' parameter available for"
  663. " 'sqlite' format only.")
  664. # Методы для проверки того, являются ли параметры наследуемыми.
  665. def is_chmod_inheritable(self, parameter_value: str) -> bool:
  666. chmod_regex = re.compile(r'\d+')
  667. if chmod_regex.search(parameter_value):
  668. return False
  669. return True
  670. def get_chown_values(self, chown: str) -> dict:
  671. """Получить значения uid и gid из параметра chown."""
  672. if chown and ':' in chown:
  673. user_name, group_name = chown.split(':')
  674. if user_name.isdigit():
  675. uid = int(user_name)
  676. else:
  677. import pwd
  678. try:
  679. if self.chroot_path == '/':
  680. uid = pwd.getpwnam(user_name).pw_uid
  681. else:
  682. uid = self.get_uid_from_passwd(user_name)
  683. except (FilesError, KeyError, TypeError) as error:
  684. raise IncorrectParameter(
  685. "'chown = {}' parameter check is failed: {}".
  686. format(chown, str(error)))
  687. if group_name.isdigit():
  688. gid = int(group_name)
  689. else:
  690. import grp
  691. try:
  692. if self.chroot_path == '/':
  693. gid = grp.getgrnam(group_name).gr_gid
  694. else:
  695. gid = self.get_gid_from_group(group_name)
  696. except (FilesError, KeyError, TypeError) as error:
  697. raise IncorrectParameter(
  698. "'chown = {}' parameter check is failed: {}".
  699. format(chown, str(error)))
  700. return {'uid': uid, 'gid': gid}
  701. else:
  702. raise IncorrectParameter("'chown' value '{0}' is not correct".
  703. format(chown))
  704. def get_uid_from_passwd(self, user_name: str) -> int:
  705. """Функция для получения uid из chroot passwd файла."""
  706. passwd_file_path = os.path.join(self.chroot_path, 'etc/passwd')
  707. passwd_dictionary = dict()
  708. if os.path.exists(passwd_file_path):
  709. with open(passwd_file_path, 'r') as passwd_file:
  710. for line in passwd_file:
  711. if line.startswith('#'):
  712. continue
  713. passwd_item = tuple(line.split(':')[0:3:2])
  714. if (len(passwd_item) > 1 and passwd_item[0]):
  715. passwd_dictionary[passwd_item[0]] = passwd_item[1]
  716. if user_name in passwd_dictionary:
  717. return int(passwd_dictionary[user_name])
  718. else:
  719. raise FilesError("'{0}' uid was not found in {1}".
  720. format(user_name, passwd_file_path))
  721. else:
  722. raise FilesError("passwd file was not found in {}".
  723. format(passwd_file_path))
  724. def get_gid_from_group(self, group_name: str) -> int:
  725. """Функция для получения gid из chroot group файла."""
  726. group_file_path = os.path.join(self.chroot_path, 'etc/group')
  727. group_dictionary = dict()
  728. if os.path.exists(group_file_path):
  729. with open(group_file_path, 'r') as group_file:
  730. for line in group_file:
  731. if line.startswith('#'):
  732. continue
  733. group_item = tuple(line.split(':')[0:3:2])
  734. if len(group_item) > 1 and group_item[0] and group_item[1]:
  735. group_dictionary[group_item[0]] = group_item[1]
  736. if group_name in group_dictionary:
  737. return int(group_dictionary[group_name])
  738. else:
  739. raise FilesError("'{0}' gid was not found in {1}".
  740. format(group_name, group_file_path))
  741. else:
  742. raise FilesError("group file was not found in {}".
  743. format(group_file_path))
  744. @classmethod
  745. def _inspect_formats_package(cls) -> None:
  746. '''Метод для определения множества доступных форматов и
  747. предоставляемых ими параметров.'''
  748. if cls.format_is_inspected:
  749. return
  750. parameters_set = set()
  751. available_formats = dict()
  752. format_directory_path = os.path.join(os.path.dirname(__file__),
  753. 'format')
  754. for module_name in os.listdir(format_directory_path):
  755. if (os.path.isdir(os.path.join(format_directory_path,
  756. module_name)) or
  757. module_name == '__init__.py'):
  758. continue
  759. if module_name.endswith('.py'):
  760. module_name = module_name[:-3]
  761. try:
  762. module = import_module('calculate.templates.format.{}'.
  763. format(module_name))
  764. for obj in dir(module):
  765. # TODO изменить способ выборки классов форматов.
  766. if obj.endswith('Format') and obj != 'BaseFormat':
  767. format_class = getattr(module, obj, False)
  768. if format_class:
  769. format_name = getattr(format_class,
  770. 'FORMAT', False)
  771. if not format_name:
  772. continue
  773. available_formats.update(
  774. {format_name: format_class})
  775. format_parameters = getattr(format_class,
  776. 'FORMAT_PARAMETERS',
  777. set())
  778. parameters_set.update(format_parameters)
  779. except Exception:
  780. continue
  781. cls.available_formats = available_formats
  782. cls.available_parameters.update(parameters_set)
  783. cls.formats_inspected = True
  784. def resolve_or_missing(context: "CalculateContext",
  785. key: str, missing=missing,
  786. env: Optional[set] = None) -> Any:
  787. '''Переопределение функции из для поиска значений переменных из jinja2.
  788. Ищет переменные в datavars.'''
  789. if env is None:
  790. env = {}
  791. datavars = context.parent['__datavars__']
  792. if key in context.vars:
  793. return context.vars[key]
  794. if key in context.parent:
  795. return context.parent[key]
  796. if key in datavars:
  797. return datavars[key]
  798. for namespace in env:
  799. if key in namespace:
  800. return namespace[key]
  801. return missing
  802. class CalculateContext(Context):
  803. '''Класс контекста позволяющий использовать значения datavars и
  804. сохранять их.'''
  805. _env_set = set()
  806. def __init__(self, environment: Environment,
  807. parent: Union[Context, Dict[str, Any]],
  808. name: str, blocks: Dict[str, Any], **kwargs):
  809. super().__init__(environment, parent, name, blocks, **kwargs)
  810. def resolve(self, key: str) -> Any:
  811. if self._legacy_resolve_mode:
  812. rv = resolve_or_missing(self, key,
  813. env=self._env_set)
  814. else:
  815. rv = self.resolve_or_missing(key)
  816. if rv is missing:
  817. return self.environment.undefined(name=key)
  818. return rv
  819. def resolve_or_missing(self, key: str) -> Any:
  820. if self._legacy_resolve_mode:
  821. rv = self.resolve(key)
  822. if isinstance(rv, Undefined):
  823. rv = missing
  824. return rv
  825. return resolve_or_missing(self, key,
  826. env=self._env_set)
  827. class ParametersContainer(MutableMapping):
  828. '''Класс для хранения параметров, взятых из шаблона, и передачи
  829. их шаблонизатору.'''
  830. def __init__(self, parameters_dictionary: Optional[dict] = None):
  831. # Слой ненаследуемых параметров.
  832. self.__parameters: dict = {}
  833. # Слой наследуемых параметров.
  834. if parameters_dictionary is not None:
  835. self.__inheritable: dict = parameters_dictionary
  836. else:
  837. self.__inheritable: dict = {}
  838. def set_parameter(self, item_to_add: dict) -> None:
  839. self.__parameters.update(item_to_add)
  840. def set_inheritable(self, item_to_add: dict) -> None:
  841. self.__inheritable.update(item_to_add)
  842. def get_inheritables(self) -> "ParametersContainer":
  843. return ParametersContainer(copy.deepcopy(self.__inheritable))
  844. def remove_not_inheritable(self) -> None:
  845. self.__parameters.clear()
  846. def print_parameters_for_debug(self) -> None:
  847. print('Parameters:')
  848. pprint(self.__parameters)
  849. print('Inherited:')
  850. pprint(self.__inheritable)
  851. def is_inherited(self, parameter_name: str) -> bool:
  852. return (parameter_name not in self.__parameters
  853. and parameter_name in self.__inheritable)
  854. def remove_parameter(self, parameter_name: str) -> None:
  855. if parameter_name in self.__parameters:
  856. self.__parameters.pop(parameter_name)
  857. elif parameter_name in self.__inheritable:
  858. self.__inheritable.pop(parameter_name)
  859. def change_parameter(self, parameter: str, value: Any) -> None:
  860. if parameter in self.__parameters:
  861. self.__parameters.update({parameter: value})
  862. elif parameter in self.__inheritable:
  863. self.__inheritable.update({parameter: value})
  864. def _clear_container(self) -> None:
  865. self.__parameters.clear()
  866. self.__inheritable.clear()
  867. def __getattr__(self, parameter_name: str) -> Any:
  868. if (parameter_name not in
  869. ParametersProcessor.available_parameters):
  870. raise IncorrectParameter("Unknown parameter: '{}'".
  871. format(parameter_name))
  872. if parameter_name in self.__parameters:
  873. return self.__parameters[parameter_name]
  874. elif parameter_name in self.__inheritable:
  875. return self.__inheritable[parameter_name]
  876. else:
  877. return False
  878. def __getitem__(self, name: str) -> Any:
  879. if name in self.__parameters:
  880. return self.__parameters[name]
  881. elif name in self.__inheritable:
  882. return self.__inheritable[name]
  883. else:
  884. return False
  885. def __setitem__(self, name: str, value: Any) -> None:
  886. self.__parameters[name] = value
  887. def __delitem__(self, name: str) -> None:
  888. if name in self.__parameters:
  889. del self.__parameters[name]
  890. if name in self.__inheritable:
  891. del self.__inheritable[name]
  892. def __iter__(self) -> Iterator[str]:
  893. return iter(set(self.__parameters).union(self.__inheritable))
  894. def __len__(self) -> int:
  895. return len(set(self.__parameters).union(self.__inheritable))
  896. def __repr__(self) -> str:
  897. return '<ParametersContainer: parameters={0}, inheritables={1}>'.\
  898. format(self.__parameters, self.__inheritable)
  899. def __contains__(self, name: str) -> bool:
  900. return name in self.__parameters or name in self.__inheritable
  901. @property
  902. def parameters(self) -> dict:
  903. return self.__parameters
  904. class CalculateExtension(Extension):
  905. '''Класс расширения для jinja2, поддерживающий теги calculate-шаблонов.'''
  906. _parameters_set = set()
  907. # Виды операций в теге save.
  908. ASSIGN, APPEND, REMOVE = range(3)
  909. def __init__(self, environment: Environment,
  910. parameters_processor: ParametersProcessor,
  911. datavars_module: Union[Datavars,
  912. NamespaceNode,
  913. Variables] = Variables(),
  914. chroot_path: str = "/"):
  915. super().__init__(environment)
  916. self.environment: Environment = environment
  917. self.package_atom_parser = PackageAtomParser(chroot_path=chroot_path)
  918. self.environment.globals.update({'pkg': self.pkg,
  919. 'grep': self.grep,
  920. 'exists': self.exists})
  921. self._datavars = datavars_module
  922. self.parameters_processor = parameters_processor
  923. self.template_type: int = DIR
  924. # Флаг, указывающий, что тег calculate уже был разобран. Нужен для
  925. # того, чтобы проверять единственность тега calculate.
  926. self.calculate_parsed: bool = False
  927. self.tags = {'calculate', 'save'}
  928. self.CONDITION_TOKENS_TYPES = {'eq', 'ne', 'lt', 'gt', 'lteq', 'gteq'}
  929. self.CONDITION_NAME_TOKENS = {'not'}
  930. self.LITERAL_TOKENS_TYPES = {'string', 'integer', 'float'}
  931. if hasattr(self._datavars, 'variables_to_save'):
  932. self.TARGET_FILES_SET =\
  933. set(self._datavars.variables_to_save.keys())
  934. else:
  935. self.TARGET_FILES_SET = set()
  936. self.parse_methods = {'calculate': self.parse_calculate,
  937. 'save': self.parse_save}
  938. def __call__(self, env: Environment) -> "CalculateExtension":
  939. # Необходимо для обеспечения возможности передать готовый объект
  940. # расширения, а не его класс.
  941. # Через функцию-фабрику такое не получается устроить, потому что при
  942. # добавлении расширения jinja2 ищет у его класса атрибут identifier.
  943. return self
  944. def parse(self, parser: Parser) -> List[nodes.Output]:
  945. self.parser = parser
  946. self.stream = parser.stream
  947. tag_token = self.stream.current.value
  948. return [self.parse_methods[tag_token]()]
  949. def parse_save(self) -> nodes.Output:
  950. '''Метод для разбора тега save, сохраняющего значение указанной
  951. переменной datavars.'''
  952. lineno = next(self.stream).lineno
  953. target_file = nodes.Const('', lineno=lineno)
  954. # Получаем имя целевого файла.
  955. if self.stream.skip_if('dot'):
  956. target_file_name = self.stream.expect('name').value
  957. if target_file_name in self.TARGET_FILES_SET:
  958. target_file = nodes.Const(target_file_name)
  959. else:
  960. raise TemplateSyntaxError("Unknown target file '{}'".
  961. format(target_file_name),
  962. lineno=lineno)
  963. # получаем список из имени переменной.
  964. module_name = self.stream.expect('name').value
  965. variable_name = [nodes.Const(module_name, lineno=lineno)]
  966. while self.stream.skip_if('dot'):
  967. name = self.stream.expect('name').value
  968. variable_name.append(nodes.Const(name, lineno=lineno))
  969. variable_name = nodes.List(variable_name, lineno=lineno)
  970. if self.stream.skip_if('assign'):
  971. return self._make_save_node(variable_name,
  972. target_file,
  973. self.ASSIGN,
  974. lineno)
  975. elif self.stream.skip_if('add') and self.stream.skip_if('assign'):
  976. return self._make_save_node(variable_name,
  977. target_file,
  978. self.APPEND,
  979. lineno)
  980. elif self.stream.skip_if('sub') and self.stream.skip_if('assign'):
  981. return self._make_save_node(variable_name,
  982. target_file,
  983. self.REMOVE,
  984. lineno)
  985. raise TemplateSyntaxError("'=' is expected in 'save' tag",
  986. lineno=lineno)
  987. def parse_calculate(self) -> nodes.Output:
  988. '''Метод для разбора тега calculate, содержащего значения параметров и
  989. условия выполнения шаблона.'''
  990. lineno = next(self.stream).lineno
  991. if self.calculate_parsed:
  992. raise TemplateSyntaxError(
  993. "template can only have one calculate tag.",
  994. lineno=lineno)
  995. expect_comma_flag = False
  996. conditions = []
  997. while self.stream.current.type != 'block_end':
  998. if expect_comma_flag:
  999. self.stream.expect('comma')
  1000. if (self.stream.current.type == 'name'
  1001. and self.stream.current.value in self._parameters_set
  1002. and self.stream.look().type != 'dot'
  1003. and self.stream.look().type not in
  1004. self.CONDITION_TOKENS_TYPES
  1005. and self.stream.current.value not in
  1006. self.CONDITION_NAME_TOKENS):
  1007. # разбираем параметр.
  1008. # pairs_list.append(self.get_parameter_node())
  1009. name_node, value_node = self._get_parameter()
  1010. check_node = self.call_method('check_parameter',
  1011. [name_node,
  1012. value_node,
  1013. nodes.ContextReference()],
  1014. lineno=lineno)
  1015. check_template_node = nodes.Template(
  1016. [nodes.Output([check_node])])
  1017. check_template_node = check_template_node.set_environment(
  1018. self.environment)
  1019. check_template = self.environment.from_string(
  1020. check_template_node)
  1021. check_template.render(__datavars__=self._datavars)
  1022. elif (self._is_variable_name(self.stream.current)
  1023. or self.stream.current.type in {'lparen', 'integer',
  1024. 'float', 'string'}
  1025. or self.stream.current.value in self.CONDITION_NAME_TOKENS):
  1026. # разбираем условие. Если условие False -- кидаем исключение.
  1027. # condition_result = self.get_condition_result()
  1028. # if not condition_result:
  1029. # raise ConditionFailed('Condition is failed',
  1030. # lineno=self.stream.current.lineno)
  1031. conditions.append(self.parse_condition())
  1032. elif self.stream.current.type == 'name':
  1033. raise TemplateSyntaxError(
  1034. f"Unknown identifier '{self.stream.current.value}'"
  1035. " in calculate tag.",
  1036. lineno=self.stream.current.lineno)
  1037. else:
  1038. raise TemplateSyntaxError(
  1039. f"Can not parse token '{self.stream.current.value}'"
  1040. " in caluculate tag.",
  1041. lineno=self.stream.current.lineno)
  1042. expect_comma_flag = True
  1043. self.parameters_processor.check_postparse_parameters()
  1044. self.check_conditions(conditions)
  1045. self.calculate_parsed = True
  1046. return nodes.Output([nodes.Const('')], lineno=lineno)
  1047. def _is_variable_name(self, token: Token) -> bool:
  1048. '''Метод для проверки токена на предмет того, что он является частью
  1049. имени переменной.'''
  1050. if not token.type == 'name':
  1051. return False
  1052. if (token.value in self._datavars.available_packages or
  1053. token.value in self.environment.globals):
  1054. return True
  1055. for namespace in self.environment.context_class._env_set:
  1056. if token.value in namespace:
  1057. return True
  1058. return False
  1059. def check_parameter(self, parameter_name: str, parameter_value: Any,
  1060. context: CalculateContext) -> str:
  1061. self.parameters_processor.check_template_parameter(
  1062. parameter_name,
  1063. parameter_value,
  1064. self.template_type,
  1065. self.stream.current.lineno)
  1066. return ''
  1067. def parse_condition(self) -> Template:
  1068. try:
  1069. condition_node = self.parser.parse_expression(with_condexpr=True)
  1070. condition_node = self.call_method(
  1071. 'set_condition_result',
  1072. [condition_node],
  1073. lineno=self.stream.current.lineno)
  1074. condition_template = nodes.Template(
  1075. [nodes.Output([condition_node])])
  1076. condition_template = condition_template.set_environment(
  1077. self.environment)
  1078. template = self.environment.from_string(condition_template)
  1079. return template
  1080. except Exception as error:
  1081. raise ConditionFailed('Error during parsing condition:{}'
  1082. .format(str(error)),
  1083. lineno=self.stream.current.lineno)
  1084. def check_conditions(self, conditions: List[Template]) -> None:
  1085. for condition in conditions:
  1086. # TODO Добавить различие провала условие из синтаксической ошибки
  1087. # или при отрицательном условии.
  1088. self.condition_result = False
  1089. try:
  1090. condition.render(__datavars__=self._datavars)
  1091. except Exception as error:
  1092. raise ConditionFailed('Error during handling condition: {}'
  1093. .format(str(error)),
  1094. lineno=self.stream.current.lineno)
  1095. if not self.condition_result:
  1096. raise ConditionFailed('Condition is failed',
  1097. lineno=self.stream.current.lineno)
  1098. # DEPRECATED
  1099. def get_condition_result(self) -> bool:
  1100. '''Метод для разбора условий из тега calculate.'''
  1101. self.condition_result = False
  1102. try:
  1103. condition_node = self.parser.parse_expression(with_condexpr=True)
  1104. condition_node = self.call_method(
  1105. 'set_condition_result',
  1106. [condition_node],
  1107. lineno=self.stream.current.lineno)
  1108. condition_template = nodes.Template(
  1109. [nodes.Output([condition_node])])
  1110. condition_template = condition_template.set_environment(
  1111. self.environment)
  1112. template = self.environment.from_string(condition_template)
  1113. template.render(__datavars__=self._datavars)
  1114. except Exception:
  1115. return False
  1116. return self.condition_result
  1117. def set_condition_result(self, condition_result: Any) -> str:
  1118. '''Метод для сохранения результата вычисления условия.'''
  1119. self.condition_result = condition_result
  1120. return ''
  1121. def _make_save_node(self, variable_name_node: nodes.List,
  1122. target_file_node: nodes.Const, optype: int,
  1123. lineno: int) -> nodes.Output:
  1124. '''Метод для создания ноды, сохраняющей переменные.'''
  1125. right_value = self.parser.parse_expression(with_condexpr=True)
  1126. optype_node = nodes.Const(optype, lineno=lineno)
  1127. save_variable_node = self.call_method('save_variable',
  1128. [variable_name_node,
  1129. right_value,
  1130. target_file_node,
  1131. optype_node,
  1132. nodes.ContextReference()],
  1133. lineno=lineno)
  1134. return nodes.Output([save_variable_node], lineno=lineno)
  1135. def save_variable(self, variable: List[str], right_value: Any,
  1136. target_file: str, optype: int,
  1137. context: CalculateContext) -> str:
  1138. '''Метод для сохранения значений переменных указанных в теге save.'''
  1139. datavars = context.parent['__datavars__']
  1140. if variable[0] not in datavars:
  1141. raise SaveError("can not save variable '{}'. The variable's"
  1142. " package '{}' is not found".format(
  1143. '.'.join(variable),
  1144. variable[0]))
  1145. modify_only = (variable[0] != 'custom')
  1146. package = datavars[variable[0]]
  1147. variable_name = variable[-1]
  1148. value_container = self._find_value_container(variable, package,
  1149. modify_only=modify_only)
  1150. # Теперь меняем знaчение переменной.
  1151. if isinstance(value_container, NamespaceNode):
  1152. self._modify_variables(variable, value_container, right_value,
  1153. optype, target_file=target_file,
  1154. modify_only=modify_only)
  1155. elif isinstance(value_container, VariableNode):
  1156. hash_value = value_container.get_value().get_hash()
  1157. if variable_name in hash_value:
  1158. if optype == self.ASSIGN:
  1159. new_value = right_value
  1160. elif optype == self.APPEND:
  1161. new_value = new_value + right_value
  1162. elif optype == self.REMOVE:
  1163. new_value = new_value - right_value
  1164. else:
  1165. new_value = right_value
  1166. hash_value.update({variable_name: new_value})
  1167. value_container.set(hash_value)
  1168. if target_file:
  1169. self._save_to_target(variable[:-1], variable_name,
  1170. new_value, target_file)
  1171. return ''
  1172. def _find_value_container(self, variable: List[str],
  1173. vars_package: NamespaceNode,
  1174. modify_only: bool = True
  1175. ) -> Union[NamespaceNode, VariableNode]:
  1176. '''Метод для поиска контейнера, путь к которому указан в аргументе.
  1177. Этим контейнером может быть пространство имен или хэш.'''
  1178. current_container = vars_package
  1179. variable_path = variable[1:-1]
  1180. for section in variable_path:
  1181. if section in current_container._namespaces:
  1182. current_container = current_container._namespaces[section]
  1183. elif (section in current_container._variables
  1184. and current_container._variables[section].variable_type
  1185. is HashType):
  1186. current_container = current_container._variables[section]
  1187. if section != variable_path[-1]:
  1188. # Если обнаружен хэш, но в пути к переменной кроме ключа
  1189. # хэша есть еще что-то далее -- значит путь к переменной
  1190. # ошибочен.
  1191. raise SaveError("can not save variable '{}'. Other"
  1192. " variable '{}' on the path".format(
  1193. '.'.join(variable),
  1194. current_container.get_fullname()))
  1195. elif not modify_only:
  1196. new_namespace = NamespaceNode(section)
  1197. current_container.add_namespace(new_namespace)
  1198. current_container = new_namespace
  1199. else:
  1200. raise SaveError("can not save variable '{}'. Namespace '{}'"
  1201. " is not found in '{}'".format(
  1202. '.'.join(variable),
  1203. section,
  1204. current_container.get_fullname()))
  1205. return current_container
  1206. def _modify_variables(self, variable: List[str], namespace: NamespaceNode,
  1207. new_value: Any, optype: int,
  1208. target_file: Optional[str] = None,
  1209. modify_only: bool = True) -> None:
  1210. '''Метод для модификации значения переменной.'''
  1211. variable_name = variable[-1]
  1212. if variable_name in namespace._variables:
  1213. variable_node = namespace[variable_name]
  1214. if optype == self.ASSIGN:
  1215. variable_node.set(new_value)
  1216. elif optype == self.APPEND:
  1217. new_value = self._append_variable_value(variable_node,
  1218. new_value)
  1219. variable_node.set(new_value)
  1220. elif optype == self.REMOVE:
  1221. new_value = self._remove_variable_value(variable_node,
  1222. new_value)
  1223. variable_node.set(new_value)
  1224. elif not modify_only:
  1225. VariableNode(variable_name, namespace,
  1226. variable_type=IniType, source=str(new_value))
  1227. else:
  1228. raise SaveError("can not create variable '{}' in not 'custom'"
  1229. " namespace".format('.'.join(variable)))
  1230. if target_file:
  1231. if namespace._variables[variable_name].variable_type is HashType:
  1232. for key, value in new_value.items():
  1233. self._save_to_target(variable, key, value, target_file)
  1234. else:
  1235. self._save_to_target(variable[:-1], variable_name,
  1236. new_value, target_file)
  1237. # DEPRECATED
  1238. def _modify_hash(self, variable_name: List[str],
  1239. hash_variable: VariableNode, new_value, optype,
  1240. target_file: Optional[str] = None) -> None:
  1241. '''Метод для модификации значения в переменной-хэше.'''
  1242. value_name = variable_name[-1]
  1243. hash_value = hash_variable.get_value().get_hash()
  1244. if value_name in hash_value:
  1245. if optype == self.APPEND:
  1246. new_value = hash_value[value_name] + new_value
  1247. elif optype == self.REMOVE:
  1248. new_value = hash_value[value_name] - new_value
  1249. hash_value.update({value_name: new_value})
  1250. hash_variable.set(hash_value)
  1251. if target_file:
  1252. self._save_to_target(variable_name[:-1], value_name,
  1253. new_value, target_file)
  1254. def _save_to_target(self, namespace_name: List[str],
  1255. variable_name: str, value: Any, target_file: str
  1256. ) -> None:
  1257. '''Метод для добавления переменной в список переменных, значение
  1258. которых было установлено через тег save и при этом должно быть
  1259. сохранено в указанном файле: save.target_file.'''
  1260. namespace_name = tuple(namespace_name)
  1261. target_file_dict = self._datavars.variables_to_save[target_file]
  1262. if namespace_name not in target_file_dict:
  1263. target_file_dict.update({namespace_name: dict()})
  1264. target_file_dict[namespace_name].update(
  1265. {variable_name: ('=', str(value))})
  1266. def _append_variable_value(self, variable: VariableNode,
  1267. value: Any) -> Any:
  1268. '''Метод описывающий операцию += в теге save.'''
  1269. variable_value = variable.get_value()
  1270. if (variable.variable_type is IntegerType or
  1271. variable.variable_type is FloatType):
  1272. variable_value += value
  1273. return variable_value
  1274. elif variable.variable_type is ListType:
  1275. if isinstance(value, str):
  1276. value = value.split(',')
  1277. if isinstance(value, list):
  1278. for item in value:
  1279. if item not in variable_value:
  1280. variable_value.append(item)
  1281. else:
  1282. variable_value.append(value)
  1283. return variable_value
  1284. elif variable.variable_type is IniType:
  1285. if not isinstance(variable_value, str):
  1286. variable_value = str(variable_value)
  1287. variable_value = variable_value.split(',')
  1288. if not isinstance(value, list):
  1289. if isinstance(value, str):
  1290. value = value.split(',')
  1291. else:
  1292. value = str(value).split(',')
  1293. for item in value:
  1294. if item not in variable_value:
  1295. variable_value.append(item)
  1296. return ','.join(variable_value)
  1297. # Пока что во всех остальных случаях будем просто возвращать исходное
  1298. # значение.
  1299. return variable_value
  1300. def _remove_variable_value(self, variable: VariableNode, value: Any
  1301. ) -> Any:
  1302. '''Метод описывающий операцию -= в теге save.'''
  1303. variable_value = variable.get_value()
  1304. if (variable.variable_type is IntegerType or
  1305. variable.variable_type is FloatType):
  1306. variable_value -= value
  1307. return variable_value
  1308. elif variable.variable_type is ListType:
  1309. if isinstance(value, str):
  1310. value = value.split(',')
  1311. if isinstance(value, list):
  1312. for item in value:
  1313. if item in variable_value:
  1314. variable_value.remove(item)
  1315. elif value in variable_value:
  1316. variable_value.remove(value)
  1317. return variable_value
  1318. elif variable.variable_type is IniType:
  1319. if not isinstance(variable_value, list):
  1320. if not isinstance(variable_value, str):
  1321. variable_value = str(variable_value)
  1322. variable_value = variable_value.split(',')
  1323. if not isinstance(value, list):
  1324. if isinstance(value, str):
  1325. value = value.split(',')
  1326. else:
  1327. value = str(value).split(',')
  1328. for item in value:
  1329. if item in variable_value:
  1330. variable_value.remove(item)
  1331. return ','.join(variable_value)
  1332. # Пока что во всех остальных случаях будем просто возвращать исходное
  1333. # значение.
  1334. return variable_value
  1335. def _get_parameter(self) -> Tuple[nodes.Const, nodes.Node]:
  1336. '''Метод для разбора параметров, содержащихся в теге calculate.'''
  1337. lineno = self.stream.current.lineno
  1338. parameter_name = self.stream.expect('name').value
  1339. parameter_name_node = nodes.Const(parameter_name, lineno=lineno)
  1340. if self.stream.skip_if('assign'):
  1341. # parameter_value = self.stream.current.value
  1342. parameter_rvalue = self.parser.parse_expression(with_condexpr=True)
  1343. # if parameter_name == 'env':
  1344. # # если параметр env -- обновляем множество значений env
  1345. # # контекста вo время парсинга.
  1346. # env_names = parameter_value.split(',')
  1347. # for name in env_names:
  1348. # self.environment.context_class._env_set.add(name.strip())
  1349. else:
  1350. parameter_rvalue = nodes.Const(True, lineno=lineno)
  1351. # parameter_value = True
  1352. return (parameter_name_node, parameter_rvalue)
  1353. def save_parameters(cls, parameters_dictionary: dict,
  1354. context: CalculateContext) -> str:
  1355. '''Метод для сохранения значений параметров.'''
  1356. context.parent['__parameters__'].set_parameter(parameters_dictionary)
  1357. return ''
  1358. @contextfunction
  1359. def pkg(self, context: CalculateContext, *args: dict) -> Version:
  1360. '''Метод, реализующий функцию pkg() шаблонов. Функция предназначена для
  1361. получения версии пакета, к которому уже привязан шаблон, если
  1362. аргументов нет, или версию пакета в аргументе функции. Если аргументов
  1363. нет, а шаблон не привязан к какому-либо пакету, или если указанного в
  1364. аргументе пакета нет -- функция возвращает пустой объект Version().'''
  1365. if args:
  1366. package_atom = args[0]
  1367. try:
  1368. atom_name = self.package_atom_parser.parse_package_parameter(
  1369. package_atom)
  1370. return atom_name.version
  1371. except PackageAtomError:
  1372. return Version()
  1373. else:
  1374. # package = context.parent['__parameters__'].package
  1375. package = self.parameters_processor._parameters_container.package
  1376. if not package:
  1377. return Version()
  1378. return package.version
  1379. def get_full_filepath(self, fname: str) -> str:
  1380. # TODO: добавить получение домашней директории пользователя
  1381. # if fname[0] == "~":
  1382. # # Получаем директорию пользователя
  1383. # fname = os.path.join(self.homeDir,
  1384. # fname.partition("/")[2], "")[:-1]
  1385. # TODO: учитывать также root_path
  1386. fname = os.path.join(
  1387. self.parameters_processor.chroot_path,
  1388. fname.lstrip("/"))
  1389. return fname
  1390. @contextfunction
  1391. def grep(self, context: CalculateContext, fname: str,
  1392. regpattern: str) -> str:
  1393. '''Метод реализующий функцию grep.'''
  1394. fname = self.get_full_filepath(fname)
  1395. try:
  1396. reg = re.compile(regpattern, re.MULTILINE)
  1397. except re.error:
  1398. raise TemplateSyntaxError("Wrong regular expression")
  1399. fileContent = readFile(fname)
  1400. if not fileContent:
  1401. return ""
  1402. match_data = reg.search(fileContent)
  1403. if match_data:
  1404. md_groups = match_data.groups()
  1405. if md_groups:
  1406. return md_groups[0] or ""
  1407. else:
  1408. return match_data.group()
  1409. else:
  1410. return ""
  1411. @contextfunction
  1412. def exists(self, context: CalculateContext, fname: str) -> str:
  1413. '''Метод реализующий функцию exists.'''
  1414. fname = self.get_full_filepath(fname)
  1415. try:
  1416. check_map = (
  1417. ('f', stat.S_ISREG),
  1418. ('d', stat.S_ISDIR),
  1419. ('l', stat.S_ISLNK),
  1420. ('b', stat.S_ISBLK),
  1421. ('c', stat.S_ISCHR),
  1422. ('p', stat.S_ISFIFO),
  1423. ('s', stat.S_ISSOCK))
  1424. fmode = os.lstat(fname)
  1425. for t, func in check_map:
  1426. if func(fmode.st_mode):
  1427. return t
  1428. else:
  1429. return 'f'
  1430. except OSError:
  1431. return ""
  1432. class TemplateEngine:
  1433. def __init__(self, directory_path: Optional[str] = None,
  1434. datavars_module: Union[Datavars,
  1435. NamespaceNode,
  1436. Variables] = Variables(),
  1437. appends_set: set = set(),
  1438. chroot_path: str = '/',
  1439. for_package: Union[Package, None] = None,
  1440. pkg_autosave: bool = False):
  1441. ParametersProcessor._inspect_formats_package()
  1442. CalculateExtension._parameters_set =\
  1443. ParametersProcessor.available_parameters
  1444. ParametersProcessor.available_appends = appends_set
  1445. self._datavars_module = datavars_module
  1446. self._template_text = ''
  1447. self._pkg_autosave: bool = pkg_autosave
  1448. self._parameters_object = ParametersContainer()
  1449. self.parameters_processor = ParametersProcessor(
  1450. chroot_path=chroot_path,
  1451. datavars_module=datavars_module,
  1452. for_package=for_package)
  1453. if directory_path is not None:
  1454. self.environment = Environment(
  1455. loader=FileSystemLoader(directory_path),
  1456. trim_blocks=True,
  1457. lstrip_blocks=True)
  1458. else:
  1459. self.environment = Environment(trim_blocks=True,
  1460. lstrip_blocks=True)
  1461. self.environment.filters.update(CALCULATE_FILTERS)
  1462. self.calculate_extension = CalculateExtension(
  1463. self.environment,
  1464. self.parameters_processor,
  1465. datavars_module=datavars_module,
  1466. chroot_path=chroot_path)
  1467. self.environment.add_extension(self.calculate_extension)
  1468. self.environment.context_class = CalculateContext
  1469. @property
  1470. def for_package(self) -> Package:
  1471. return self.parameters_processor.for_package
  1472. @for_package.setter
  1473. def for_package(self, package: Package) -> None:
  1474. self.parameters_processor.for_package = package
  1475. def change_directory(self, directory_path: str) -> None:
  1476. '''Метод для смены директории в загрузчике.'''
  1477. self.environment.loader = FileSystemLoader(directory_path)
  1478. def process_template(self, template_path: str, template_type: str,
  1479. parameters: Optional[ParametersContainer] = None
  1480. ) -> None:
  1481. '''Метод для обработки файла шаблона, расположенного по указанному
  1482. пути.'''
  1483. if parameters is not None:
  1484. self._parameters_object = parameters
  1485. else:
  1486. self._parameters_object = ParametersContainer()
  1487. if self._parameters_object.env:
  1488. CalculateContext._env_set = self._parameters_object.env.copy()
  1489. else:
  1490. CalculateContext._env_set = set()
  1491. self.parameters_processor.set_parameters_container(
  1492. self._parameters_object)
  1493. self.calculate_extension.template_type = template_type
  1494. self.calculate_extension.calculate_parsed = False
  1495. template = self.environment.get_template(template_path)
  1496. self._template_text = template.render(
  1497. __datavars__=self._datavars_module,
  1498. __parameters__=self._parameters_object,
  1499. Version=Version
  1500. )
  1501. def process_template_from_string(
  1502. self, string: str, template_type: int,
  1503. parameters: Optional[ParametersContainer] = None
  1504. ) -> None:
  1505. '''Метод для обработки текста шаблона.'''
  1506. if parameters is not None:
  1507. self._parameters_object = parameters
  1508. else:
  1509. self._parameters_object = ParametersContainer()
  1510. if self._parameters_object.env:
  1511. CalculateContext._env_set = self._parameters_object.env.copy()
  1512. else:
  1513. CalculateContext._env_set = set()
  1514. self.parameters_processor.set_parameters_container(
  1515. self._parameters_object)
  1516. self.calculate_extension.template_type = template_type
  1517. self.calculate_extension.calculate_parsed = False
  1518. template = self.environment.from_string(string)
  1519. self._template_text = template.render(
  1520. __datavars__=self._datavars_module,
  1521. __parameters__=self._parameters_object,
  1522. Version=Version
  1523. )
  1524. @property
  1525. def parameters(self) -> ParametersContainer:
  1526. return self._parameters_object
  1527. @property
  1528. def template_text(self) -> str:
  1529. text, self._template_text = self._template_text, ''
  1530. return text