io_ctrl.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. """
  2. 输入输出的底层实现函数
  3. """
  4. import inspect
  5. import json
  6. import logging
  7. from functools import partial, wraps
  8. from collections import UserList
  9. from .session import chose_impl, next_client_event, get_current_task_id, get_current_session
  10. logger = logging.getLogger(__name__)
  11. class Output:
  12. """ ``put_xxx()`` 类函数的返回值
  13. 若 ``put_xxx()`` 调用的返回值没有被变量接收,则直接将消息发送到会话;
  14. 否则消息则作为其他消息的一部分
  15. """
  16. @staticmethod
  17. def jsonify(data):
  18. return json.loads(json.dumps(data, default=output_json_encoder))
  19. @staticmethod
  20. def safely_destruct(obj):
  21. """安全销毁 OutputReturn 对象/包含OutputReturn对象的dict/list, 使 OutputReturn.__del__ 不进行任何操作"""
  22. try:
  23. json.dumps(obj, default=partial(output_json_encoder, ignore_error=True))
  24. except Exception:
  25. pass
  26. def __init__(self, spec, on_embed=None):
  27. self.processed = False
  28. self.on_embed = on_embed or (lambda d: d)
  29. try:
  30. # todo 使用其他方式来转换spec
  31. self.spec = json.loads(json.dumps(spec, default=output_json_encoder)) # this may raise TypeError
  32. except TypeError:
  33. self.processed = True #
  34. type(self).safely_destruct(spec)
  35. raise
  36. def embed_data(self):
  37. """返回供嵌入到其他消息中的数据,可以设置一些默认值"""
  38. self.processed = True
  39. return self.on_embed(self.spec)
  40. def send(self):
  41. """发送输出内容到Client"""
  42. if not self.processed:
  43. send_msg('output', self.spec)
  44. self.processed = True
  45. def __del__(self):
  46. """返回值没有被变量接收时的操作:直接输出消息"""
  47. self.send()
  48. class OutputList(UserList):
  49. def __del__(self):
  50. """返回值没有被变量接收时的操作:直接输出消息"""
  51. for o in self.data:
  52. o.send()
  53. def output_json_encoder(obj, ignore_error=False):
  54. """json序列化与输出相关消息的Encoder函数 """
  55. if isinstance(obj, Output):
  56. return obj.embed_data()
  57. elif isinstance(obj, OutputList):
  58. return obj.data
  59. if not ignore_error:
  60. raise TypeError('Object of type %s is not JSON serializable' % obj.__class__.__name__)
  61. def safely_destruct_output_when_exp(content_param):
  62. """装饰器生成: 异常时安全释放 OutputReturn 对象
  63. :param content_param: 含有OutputReturn实例的参数名或参数名列表
  64. :type content_param: list/str
  65. :return: 装饰器
  66. """
  67. def decorator(func):
  68. sig = inspect.signature(func)
  69. @wraps(func)
  70. def inner(*args, **kwargs):
  71. try:
  72. return func(*args, **kwargs)
  73. except Exception:
  74. # 发生异常,安全地释放 OutputReturn 对象
  75. params = [content_param] if isinstance(content_param, str) else content_param
  76. bound = sig.bind(*args, **kwargs).arguments
  77. for param in params:
  78. if bound.get(param):
  79. Output.safely_destruct(bound.get(param))
  80. raise
  81. return inner
  82. return decorator
  83. def send_msg(cmd, spec=None):
  84. msg = dict(command=cmd, spec=spec, task_id=get_current_task_id())
  85. get_current_session().send_task_command(msg)
  86. @chose_impl
  87. def single_input(item_spec, valid_func, preprocess_func):
  88. """
  89. Note: 鲁棒性在上层完成
  90. 将单个input构造成input_group,并获取返回值
  91. :param item_spec: 单个输入项的参数 'name' must in item_spec, 参数一定已经验证通过
  92. :param valid_func: Not None
  93. :param preprocess_func: Not None
  94. """
  95. if item_spec.get('name') is None: # single input
  96. item_spec['name'] = 'data'
  97. else: # as input_group item
  98. return dict(item_spec=item_spec, valid_func=valid_func, preprocess_func=preprocess_func)
  99. label = item_spec['label']
  100. name = item_spec['name']
  101. # todo 是否可以原地修改spec
  102. item_spec['label'] = ''
  103. item_spec.setdefault('auto_focus', True) # 如果没有设置autofocus参数,则开启参数 todo CHECKBOX, RADIO 特殊处理
  104. spec = dict(label=label, inputs=[item_spec])
  105. data = yield input_control(spec, {name: preprocess_func}, {name: valid_func})
  106. return data[name]
  107. @chose_impl
  108. def input_control(spec, preprocess_funcs, item_valid_funcs, form_valid_funcs=None):
  109. """
  110. 发送input命令,监听事件,验证输入项,返回结果
  111. :param spec:
  112. :param preprocess_funcs: keys 严格等于 spec中的name集合
  113. :param item_valid_funcs: keys 严格等于 spec中的name集合
  114. :param form_valid_funcs:
  115. :return:
  116. """
  117. send_msg('input_group', spec)
  118. data = yield input_event_handle(item_valid_funcs, form_valid_funcs, preprocess_funcs)
  119. send_msg('destroy_form')
  120. return data
  121. def check_item(name, data, valid_func, preprocess_func):
  122. try:
  123. data = preprocess_func(data)
  124. error_msg = valid_func(data)
  125. except Exception as e:
  126. logger.warning('Get %r in valid_func for name:"%s"', e, name)
  127. error_msg = '字段内容不合法'
  128. if error_msg is not None:
  129. send_msg('update_input', dict(target_name=name, attributes={
  130. 'valid_status': False,
  131. 'invalid_feedback': error_msg
  132. }))
  133. return False
  134. return True
  135. @chose_impl
  136. def input_event_handle(item_valid_funcs, form_valid_funcs, preprocess_funcs):
  137. """
  138. 根据提供的校验函数处理表单事件
  139. :param item_valid_funcs: map(name -> valid_func) valid_func 为 None 时,不进行验证
  140. valid_func: callback(data) -> error_msg or None
  141. :param form_valid_funcs: callback(data) -> (name, error_msg) or None
  142. :param preprocess_funcs:
  143. :return:
  144. """
  145. while True:
  146. event = yield next_client_event()
  147. event_name, event_data = event['event'], event['data']
  148. if event_name == 'input_event':
  149. input_event = event_data['event_name']
  150. if input_event == 'blur':
  151. onblur_name = event_data['name']
  152. check_item(onblur_name, event_data['value'], item_valid_funcs[onblur_name],
  153. preprocess_funcs[onblur_name])
  154. elif event_name == 'from_submit':
  155. all_valid = True
  156. # 调用输入项验证函数进行校验
  157. for name, valid_func in item_valid_funcs.items():
  158. if not check_item(name, event_data[name], valid_func, preprocess_funcs[name]):
  159. all_valid = False
  160. if all_valid: # todo 减少preprocess_funcs[name]调用次数
  161. data = {name: preprocess_funcs[name](val) for name, val in event_data.items()}
  162. # 调用表单验证函数进行校验
  163. if form_valid_funcs:
  164. v_res = form_valid_funcs(data)
  165. if v_res is not None:
  166. all_valid = False
  167. onblur_name, error_msg = v_res
  168. send_msg('update_input', dict(target_name=onblur_name, attributes={
  169. 'valid_status': False,
  170. 'invalid_feedback': error_msg
  171. }))
  172. if all_valid:
  173. break
  174. elif event_name == 'from_cancel':
  175. data = None
  176. break
  177. else:
  178. logger.warning("Unhandled Event: %s", event)
  179. return data
  180. def output_register_callback(callback, **options):
  181. task_id = get_current_session().register_callback(callback, **options)
  182. return task_id