io_ctrl.py 7.2 KB

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