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.

712 lines
30 KiB

  1. # vim: fileencoding=utf-8
  2. #
  3. from ..variables.datavars import DependenceAPI, VariableWrapper, NamespaceNode
  4. from ..variables.loader import Datavars
  5. from collections import OrderedDict
  6. from contextlib import contextmanager
  7. from typing import Tuple, Union, Any
  8. class ParameterError(Exception):
  9. pass
  10. class ValidationError(ParameterError):
  11. pass
  12. class CyclicValidationError(ValidationError):
  13. def __init__(self, *queue):
  14. self.queue = queue
  15. def __str__(self):
  16. return "Cyclic validation for parameters: {}".format(", ".join(
  17. self.queue[:-1]))
  18. class ParameterType:
  19. '''Общий класс типов параметров.'''
  20. descr = 'VALUE'
  21. def process_value(self, value, parameter):
  22. return value
  23. class Integer(ParameterType):
  24. '''Класс типа целочисленных параметров.'''
  25. descr = 'INTEGER'
  26. def process_value(self, value, parameter) -> int:
  27. if isinstance(value, int):
  28. return value
  29. raise ValidationError(f"Can not assign '{type(value)}' value to"
  30. " parameter of the int type")
  31. def __repr__(self):
  32. return '<Integer Parameter Type>'
  33. class String(ParameterType):
  34. '''Класс типа строковых параметров.'''
  35. descr = 'STRING'
  36. def process_value(self, value, parameter) -> str:
  37. if isinstance(value, str):
  38. return value
  39. raise ValidationError(f"Can not assign '{type(value)}' value to"
  40. " parameter of the string type")
  41. def __repr__(self):
  42. return '<String Parameter Type>'
  43. class Bool(ParameterType):
  44. '''Класс типа строковых параметров.'''
  45. false_set = {'n', 'false', 'False', 'no'}
  46. true_set = {'y', 'true', 'True', 'yes'}
  47. descr = '[y/n]'
  48. def process_value(self, value, parameter) -> bool:
  49. if isinstance(value, bool):
  50. return value
  51. raise ValidationError(f"Can not assign '{type(value)}' value to"
  52. " parameter of the bool type")
  53. def __repr__(self):
  54. return '<Bool Parameter Type>'
  55. class Float(ParameterType):
  56. '''Класс типа вещественночисловых параметров.'''
  57. descr = 'FLOAT'
  58. def process_value(self, value, parameter) -> float:
  59. if isinstance(value, float):
  60. return value
  61. raise ValidationError(f"Can not assign '{type(value)}' value to"
  62. " parameter of the float type")
  63. def __repr__(self):
  64. return '<Float Parameter Type>'
  65. class Separator:
  66. '''Класс объекта сепаратора, возможно, его не будет'''
  67. def __repr__(self):
  68. return "# # #"
  69. class Choice(ParameterType):
  70. '''Класс типа, представлющий собой любое значение из совокупности заданных.
  71. '''
  72. descr = 'CHOICES'
  73. def __init__(self, choices=OrderedDict(), editable=False,
  74. multichoice=False):
  75. self.editable = editable
  76. self.multichoice = multichoice
  77. self.choices = choices
  78. def add_choice(self, choice, comment):
  79. self.choices.update({choice: comment})
  80. def process_value(self, value, parameter):
  81. if value in self.choices:
  82. return value
  83. else:
  84. raise ValidationError(f"value '{value}' is not available in"
  85. f" parameter '{parameter._name}' available"
  86. f" values: '{', '.join(self.choices)}'")
  87. def __repr__(self):
  88. return (f'<Choice Parameter Type: choices={", ".join(self.choices)},'
  89. f' editable={self.editable}, multichoice={self.multichoice}>')
  90. class List(ParameterType):
  91. descr = 'LIST'
  92. def process_value(self, value, parameter):
  93. if isinstance(value, list):
  94. return value
  95. raise ValidationError(f"Can not assign '{type(value)}' value to"
  96. " parameter of the list type")
  97. def __repr__(self):
  98. return ('<List Parameter Type>')
  99. class TableValue:
  100. '''Класс значения таблицы.'''
  101. def __init__(self, key, fields, fill_function=lambda x: x):
  102. self._fields = fields
  103. self._values = OrderedDict()
  104. # Родительский параметр.
  105. self._parent = None
  106. # Ключ и его индекс в массиве.
  107. self.primary_key = key
  108. # Устанавливаем комментарии стобцам таблицы, по умолчанию то же, что и
  109. # название столбца.
  110. self._fields_comments = OrderedDict()
  111. for field in self._fields:
  112. self._fields_comments.update({field: field})
  113. # Функция для заполнения пустых полей таблицы.
  114. self.fill = fill_function
  115. # Функция для кастомизации уведомления об ошибке.
  116. def set_error(value: str, field: str, available: list) -> None:
  117. raise ParameterError(f"{field} '{value}' is not found.")
  118. self.set_error = set_error
  119. def set_comments(self, *comments):
  120. '''Метод для установки комментариев к полям таблицы.'''
  121. if len(comments) < len(self._fields):
  122. raise ParameterError("Not enough values to comment table fields.")
  123. elif len(comments) > len(self._fields):
  124. raise ParameterError("Too much values to comment table fields.")
  125. self._fields_comments = OrderedDict()
  126. for field, comment in zip(self._fields, comments):
  127. if comment is not None:
  128. self._fields_comments[field] = comment
  129. def change(self, *values):
  130. '''Метод для добавления новых или изменения существующих строк таблицы.
  131. '''
  132. if len(values) < len(self._fields):
  133. raise ParameterError("Not enough values to fill table row.")
  134. elif len(values) > len(self._fields):
  135. raise ParameterError("Too much values to fill table row.")
  136. # Проверяем типы устанавливаемых значений.
  137. validated_values = OrderedDict()
  138. for value, (field, field_type) in zip(values,
  139. self._fields.items()):
  140. if value is None or self._parent is None:
  141. validated_values.update({field: value})
  142. else:
  143. validated_values.update({field: field_type.process_value(
  144. value,
  145. self._parent)})
  146. current_key = validated_values[self.primary_key]
  147. if current_key in self._values:
  148. self._values[current_key] = validated_values
  149. elif self._parent is None or self._parent._parameter_type.expandable:
  150. self._values.update({current_key: validated_values})
  151. else:
  152. self.set_error(current_key,
  153. self._fields_comments[self.primary_key],
  154. list(self._values.keys()))
  155. self._values = self.fill(self._values)
  156. def check_values_types(self):
  157. '''Метод для проверки по типам всех значений таблицы.'''
  158. for row in self._values.values():
  159. for field, field_type in self._fields.items():
  160. if row[field] is not None:
  161. row[field] = field_type.process_value(row[field],
  162. self._parent)
  163. def get_for_var(self):
  164. '''Метод для преобразования данного описания таблицы в ту, которая
  165. приемлема для инициализации переменных.'''
  166. output = []
  167. for value in self._values.values():
  168. output.append(value)
  169. return output
  170. class Table(ParameterType):
  171. '''Метод реализующий тип таблиц.'''
  172. descr = 'LIST'
  173. def __init__(self, expandable=False):
  174. self.expandable = expandable
  175. def process_value(self, value: TableValue, parameter):
  176. if isinstance(value, TableValue):
  177. # Если родитель не установлен -- значит параметр был только что
  178. # инициализирован. Устанавливаем его, и проводим полную проверку
  179. # инициализированных значений таблицы.
  180. if value._parent is None:
  181. value._parent = parameter
  182. value.check_values_types()
  183. return value
  184. raise ValidationError(f"Can not assign '{type(value)}' value to"
  185. " parameter of the table type")
  186. def __repr__(self):
  187. return (f'<Table Parameter Type: expandable={self.expandable}>')
  188. class File(ParameterType):
  189. pass
  190. class Password(ParameterType):
  191. '''Класс типа строковых параметров.'''
  192. descr = 'PASSWORD'
  193. def __init__(self, param=False):
  194. # TODO разобраться с параметрами, необходимыми для параметров типа
  195. # Password.
  196. self.param = param
  197. def process_value(self, value, parameter) -> str:
  198. if isinstance(value, str):
  199. return value
  200. raise ValidationError(f"Can not assign '{type(value)}' value to"
  201. " parameter of the string type")
  202. def __repr__(self):
  203. return '<String Parameter Type>'
  204. class Description:
  205. '''Класс, содержащий описание параметра и способ его отображения.'''
  206. # Словарь со способами отображения параметра в графическом интерфейсе.
  207. representations = {}
  208. def __init__(self, short='', full='', usage=''):
  209. self.short = short
  210. self.full = full
  211. self.usage = usage
  212. self._gui_repr = None
  213. def initialize(self, parameter_name, parameter_type, shortname=None):
  214. '''Метод для инициализации представления по указанной пользователем
  215. информации, типу параметра и его имени.'''
  216. if not self.short:
  217. self.short = parameter_name
  218. if not self.full:
  219. self.full = self.short
  220. if not self.usage:
  221. if shortname:
  222. self.usage = f"-{shortname} {parameter_type.descr}, "
  223. self.usage = self.usage +\
  224. f"--{parameter_name} {parameter_type.descr}"
  225. self._gui_repr = self._get_gui_repr(parameter_type)
  226. def _get_gui_repr(self, parameter_type):
  227. '''Метод для получения по типу параметра способа его представления.'''
  228. # TODO реализовать его, когда удастся выделить совокупность доступных
  229. # способов представления.
  230. return None
  231. @property
  232. def gui_repr(self):
  233. return self._gui_repr
  234. class BaseParameter:
  235. '''Класс базовый класс всех параметров.'''
  236. default_type = ParameterType
  237. def __init__(self, name, group, desctiption: Description,
  238. shortname=None, argv=None):
  239. self._group = group
  240. self._name = name
  241. self._shortname = shortname
  242. self._argv = argv
  243. # Ссылка на экземпляр контейнера параметров.
  244. self.container = None
  245. self._value = None
  246. # Допустимые значения параметра, если таковые имеются.
  247. # self.choices = OrderedDict()
  248. # Комментарии, если таковой необходим.
  249. self.comment = None
  250. # Флаг, указывающий на то, что значение параметра было изменено
  251. # установлено пользователем с помощью метода set.
  252. self._set_by_user = False
  253. self._validated = False
  254. if hasattr(self, 'type'):
  255. self._parameter_type = self.type
  256. else:
  257. self._parameter_type = self.default_type()
  258. # Добавляем описание и инициализируем его.
  259. self._description = desctiption
  260. self._description.initialize(self._name, self._parameter_type,
  261. shortname=self._shortname)
  262. # Комментарий, наличие которого указывает на неактивность параметра.
  263. self.disactivity_comment = None
  264. # Коллекции текущих подписок, аргументов, из которых эти подписки
  265. # формируются и подписчиков.
  266. self._subscriptions = {}
  267. self._args = []
  268. self._subscribers = []
  269. # Флаги для проверки того необходимо ли проводить поиск подписок или
  270. # подписчиков.
  271. self._args_is_found = False
  272. self._sets_is_found = False
  273. # Метод для указания того, что в данный момент осуществляется расчет
  274. # значения параметра.
  275. self._calculating = False
  276. self._validation = False
  277. @property
  278. def choices(self):
  279. if isinstance(self._parameter_type, Choice):
  280. return self._parameter_type.choices
  281. @property
  282. def disactivity(self):
  283. return bool(self.disactivity_comment)
  284. def validate(self, container, datavars, value) -> None:
  285. '''Метод для проверки корректности параметра. Переопределяется при
  286. создании нового типа параметра.'''
  287. return None
  288. def validate_value(self, value):
  289. '''Метод для запуска валидации параметра с использованием
  290. переопределенного метода валидации.'''
  291. if self._validation:
  292. raise CyclicValidationError(self._name)
  293. with self._start_validation():
  294. try:
  295. self.validate(self.container, self.container.datavars, value)
  296. except CyclicValidationError as error:
  297. raise CyclicValidationError(self._name, *error.queue)
  298. @contextmanager
  299. def _start_validation(self):
  300. '''Контекстный менеджер предназначеный для переключения параметра в
  301. режим валидации.'''
  302. try:
  303. self._validation = True
  304. yield self
  305. finally:
  306. self._validation = False
  307. def set(self, value) -> None:
  308. '''Метод для установки значения параметра.'''
  309. if isinstance(self._parameter_type, Table):
  310. print('VALUE:', self.value)
  311. self._value.change(*value)
  312. else:
  313. self._value = value
  314. self._set_by_user = True
  315. # Назначаем новые значения подписанным переменным.
  316. self._set_variables()
  317. @property
  318. def value(self):
  319. '''Метод для получения значения параметра. Выдается или дефолтное или
  320. установленное пользователем.'''
  321. if not self._set_by_user:
  322. if self._value is None and not self._calculating:
  323. self._value = self.update_value()
  324. return self._value
  325. def check_value_type(self, value):
  326. '''Метод для запуска проверки значения по типу параметра.'''
  327. return self._parameter_type.process_value(value, self)
  328. def bind(self, *variables) -> None:
  329. '''Метод для настройки взаимосвязи параметра с переменными.'''
  330. self._args = variables
  331. return self
  332. def bind_method(self, *variables):
  333. '''Метод переопределяемый для создания нового типа параметра. Должен
  334. возвращать вычисленное значение и активность параметра None -- активен,
  335. comment -- дизактивирован.'''
  336. if variables:
  337. return variables[0].value, None
  338. else:
  339. return None, None
  340. def set_to(self, *variables):
  341. '''Метод для добавления переменных, которым будет установлено значение
  342. параметра.'''
  343. self._subscribers = variables
  344. return self
  345. def _set_variables(self):
  346. '''Метод для установки значения параметра переменным-подписчикам.'''
  347. # Только если значение установлено пользователем.
  348. if self._set_by_user:
  349. if not self._sets_is_found:
  350. self._subscribers = self.find_variables(self._subscribers)
  351. self._sets_is_found = True
  352. for subscriber in self._subscribers:
  353. if isinstance(self._parameter_type, Table):
  354. subscriber.set(self.value.get_for_var())
  355. else:
  356. subscriber.set(self.value)
  357. def _invalidate(self):
  358. '''Метод, через который переменные сообщают о необходимости пересчитать
  359. значение параметра с дефолтным значением.'''
  360. value = self.update_value()
  361. if not self._set_by_user:
  362. self._value = value
  363. self._validated = False
  364. def update_value(self):
  365. '''Метод для получения дефолтного значения.'''
  366. if not self._args_is_found:
  367. self._args = self.find_variables(self._args)
  368. self._args_is_found = True
  369. with self._start_calculate():
  370. value, subscriptions = self._calculate_binding()
  371. # Обновляем подписки. Сначала убираем лишние.
  372. for subscription in self._subscriptions:
  373. if subscription not in subscriptions:
  374. subscription.subscribers.remove(self)
  375. # Теперь добавляем новые.
  376. for subscription in subscriptions:
  377. subscription.subscribers.add(self)
  378. self._subscriptions = subscriptions
  379. return self._parameter_type.process_value(value, self)
  380. def _calculate_binding(self):
  381. '''Метод для расчета значения параметра по умолчанию, используя
  382. заданные переменные.'''
  383. subscriptions = set()
  384. args = tuple(VariableWrapper(arg, subscriptions) for arg in self._args)
  385. try:
  386. value, self.disactivity_comment = self.bind_method(*args)
  387. return value, subscriptions
  388. except Exception as error:
  389. raise ParameterError('can not calculate using dependencies: {}'
  390. ' reason: {}'.format(', '.join(
  391. [subscription.get_fullname()
  392. for subscription
  393. in self._args]),
  394. str(error)))
  395. @contextmanager
  396. def _start_calculate(self):
  397. '''Менеджер контекста устанавливающий флаг, указывающий, что данная
  398. переменная в состоянии расчета.'''
  399. try:
  400. self._calculating = True
  401. yield self
  402. finally:
  403. self._calculating = False
  404. def find_variables(self, variables):
  405. '''Метод для поиска переменных, по их названию. Обходит список
  406. переменных и заменяет все строки с запросами в списке на результат
  407. поиска по запросу.'''
  408. output = []
  409. for variable in variables:
  410. if isinstance(variable, str):
  411. variable = DependenceAPI.find_variable(variable,
  412. self.container.datavars)
  413. output.append(variable)
  414. return output
  415. def __repr__(self):
  416. description = self._description.short or self._description.full
  417. return (f"<Parameter: '{self._name}' = "
  418. f"{self._value if self._value else 'NULL'}: "
  419. f"{description}>")
  420. class GroupWrapper:
  421. '''Класс обертки параметров группы, который возможно будет использоваться
  422. для организации работы графического интерфейса.'''
  423. def __init__(self, group, container):
  424. self.values = OrderedDict()
  425. self.container = container
  426. def set_group(self):
  427. '''Метод для установки значений параметров, осносящихся к группе.'''
  428. self.container.set_parameters(self.values)
  429. class Parameters:
  430. '''Класс контейнера параметров.'''
  431. def __init__(self, datavars: Datavars, check_order=[]):
  432. self._datavars = datavars
  433. self._parameters = OrderedDict()
  434. self._validation_dict = OrderedDict()
  435. self._order = check_order
  436. # Флаг указывающий на то, что в данный момент идет валидация параметров
  437. self._validation = False
  438. # Занятые имена параметров.
  439. self._names = set()
  440. # Список позиционных аргументов.
  441. self._argvs = []
  442. # Словарь функций для взаимодействия графического клиента с группами
  443. # параметров.
  444. self._gui_helpers = {}
  445. def add(self, *parameters: Tuple[BaseParameter]):
  446. '''Метод для добавления некоторой совокупности параметров.'''
  447. for parameter in parameters:
  448. self.add_parameter(parameter)
  449. def set_order(self, *parameters):
  450. '''Метод для установки порядка проверки параметров.'''
  451. self._order = []
  452. for parameter in parameters:
  453. if isinstance(parameter, str):
  454. parameter = self[parameter]
  455. self._order.append(parameter)
  456. for parameter in self:
  457. if parameter not in self._order:
  458. self._order.append(parameter)
  459. def add_parameter(self, parameter: BaseParameter) -> None:
  460. '''Метод для добавления параметров в контейнер.'''
  461. if parameter._name in self._names:
  462. raise ParameterError(f"Can not add parameter '{parameter._name}'."
  463. " Such name has already been added. ")
  464. elif parameter._shortname and parameter._shortname in self._names:
  465. raise ParameterError(f"Can not add parameter '{parameter._name}'."
  466. " Such name has already been added. ")
  467. elif (parameter._argv is not None and
  468. len(self._argvs) > parameter._argv and
  469. self._argvs[parameter._argv]):
  470. raise ParameterError(f"Can not add positional parameter "
  471. f"'{parameter._name}'. Position"
  472. f"'{parameter._argv}' is already taken.")
  473. parameter.container = self
  474. if parameter._group in self._parameters:
  475. self._parameters[parameter._group].append(parameter)
  476. else:
  477. self._parameters[parameter._group] = [parameter]
  478. self._names.add(parameter._name)
  479. if parameter._shortname:
  480. self._names.add(parameter._shortname)
  481. if parameter._argv is not None:
  482. # Если нужно, расширяем список позиций.
  483. if len(self._argvs) <= parameter._argv:
  484. while(len(self._argvs) <= parameter._argv):
  485. self._argvs.append(None)
  486. self._argvs[parameter._argv] = parameter
  487. self._order.append(parameter)
  488. parameter.update_value()
  489. def set_parameters(self, parameters: OrderedDict) -> None:
  490. '''Метод для установки значений некоторого числа параметров их
  491. проверки.'''
  492. # Сначала проверяем все значения по типам и составляем словарь
  493. # валидации.
  494. for parameter_name, value in parameters.items():
  495. parameter = self[parameter_name]
  496. if not parameter.disactivity_comment:
  497. self._validation_dict[parameter] =\
  498. parameter._parameter_type.process_value(value,
  499. parameter)
  500. parameter._validated = False
  501. # Теперь запускаем переопределенный метод для проверки параметров.
  502. with self._run_validation():
  503. self.validate_parameters(self._validation_dict)
  504. def validate_parameters(self, parameters: OrderedDict):
  505. '''Метод для запуска валидации параметров.'''
  506. for parameter in self._order:
  507. if parameter in self._validation_dict:
  508. if (not parameter._validated
  509. and not parameter.disactivity_comment):
  510. parameter.validate_value(self._validation_dict[parameter])
  511. parameter._validated = True
  512. parameter.set(self._validation_dict[parameter])
  513. def validate_all(self):
  514. for parameter in self._order:
  515. if (not parameter._validated and
  516. not parameter.disactivity_comment):
  517. parameter.validate_value(self._validation_dict[parameter])
  518. parameter._validated = True
  519. parameter.set(self._validation_dict[parameter])
  520. @contextmanager
  521. def _run_validation(self):
  522. '''Контекстный менеджер предназначенный для перевода контейнера
  523. параметров в режим валидации.'''
  524. try:
  525. self._validation = True
  526. yield self
  527. finally:
  528. self._validation = False
  529. self._validation_dict = OrderedDict()
  530. def get_group_parameters(self, group_name: str):
  531. '''Метод для получения списка параметров, относящихся к указанной
  532. группе.'''
  533. return self._parameters[group_name]
  534. def get_descriptions(self) -> dict:
  535. '''Метод для получения словаря с описанием параметров.'''
  536. output = OrderedDict()
  537. for group, parameters in self._parameters.items():
  538. for parameter in parameters:
  539. usage = ' ' + parameter._description.usage
  540. full = parameter._description.full
  541. if len(usage) > 23:
  542. usage = usage + '\n'
  543. full = ' ' * 24 + full
  544. elif len(usage) == 23:
  545. usage = usage + ' '
  546. else:
  547. usage = usage.ljust(23) + ' '
  548. parameter_description = (f"{usage}{full}")
  549. if group in output:
  550. output[group].append(parameter_description)
  551. else:
  552. output[group] = [parameter_description]
  553. return output
  554. @property
  555. def datavars(self) -> Union[Datavars, NamespaceNode]:
  556. return self._datavars
  557. def __getitem__(self, name: str) -> Any:
  558. for group, parameters in self._parameters.items():
  559. for parameter in parameters:
  560. if parameter._name == name or parameter._shortname == name:
  561. if self._validation and not parameter._validated:
  562. # В режиме валидации параметров, при попытке получить
  563. # параметр сначала проверяем необходимость его проверки
  564. # И если нужно проверяем его.
  565. parameter_value =\
  566. self._validation_dict.get(parameter, None) or\
  567. parameter._value
  568. parameter.validate_value(parameter_value)
  569. parameter._validated = True
  570. return parameter
  571. else:
  572. return parameter
  573. raise ParameterError(f"No such parameter '{name}'.")
  574. def __iter__(self):
  575. for parameters in self._parameters.values():
  576. for parameter in parameters:
  577. yield parameter