markup.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. from six import string_types, iteritems
  2. from functools import partial
  3. import itertools
  4. import importlib
  5. from ..core import Machine, Enum
  6. import numbers
  7. class MarkupMachine(Machine):
  8. # Special attributes such as NestedState._name/_parent or Transition._condition are handled differently
  9. state_attributes = ['on_exit', 'on_enter', 'ignore_invalid_triggers', 'timeout', 'on_timeout', 'tags']
  10. transition_attributes = ['source', 'dest', 'prepare', 'before', 'after']
  11. def __init__(self, *args, **kwargs):
  12. self._markup = kwargs.pop('markup', {})
  13. self._auto_transitions_markup = kwargs.pop('auto_transitions_markup', False)
  14. self.skip_references = True
  15. self._needs_update = True
  16. if self._markup:
  17. models_markup = self._markup.pop('models', [])
  18. super(MarkupMachine, self).__init__(None, **self._markup)
  19. for m in models_markup:
  20. self._add_markup_model(m)
  21. else:
  22. super(MarkupMachine, self).__init__(*args, **kwargs)
  23. self._markup['before_state_change'] = [x for x in (rep(f) for f in self.before_state_change) if x]
  24. self._markup['after_state_change'] = [x for x in (rep(f) for f in self.before_state_change) if x]
  25. self._markup['prepare_event'] = [x for x in (rep(f) for f in self.prepare_event) if x]
  26. self._markup['finalize_event'] = [x for x in (rep(f) for f in self.finalize_event) if x]
  27. self._markup['send_event'] = self.send_event
  28. self._markup['auto_transitions'] = self.auto_transitions
  29. self._markup['ignore_invalid_triggers'] = self.ignore_invalid_triggers
  30. self._markup['queued'] = self.has_queue
  31. @property
  32. def auto_transitions_markup(self):
  33. return self._auto_transitions_markup
  34. @auto_transitions_markup.setter
  35. def auto_transitions_markup(self, value):
  36. self._auto_transitions_markup = value
  37. self._needs_update = True
  38. @property
  39. def markup(self):
  40. self._markup['models'] = self._convert_models()
  41. return self.get_markup_config()
  42. # the only reason why this not part of markup property is that pickle
  43. # has issues with properties during __setattr__ (self.markup is not set)
  44. def get_markup_config(self):
  45. if self._needs_update:
  46. self._convert_states_and_transitions(self._markup)
  47. self._needs_update = False
  48. return self._markup
  49. def add_transition(self, trigger, source, dest, conditions=None,
  50. unless=None, before=None, after=None, prepare=None, **kwargs):
  51. super(MarkupMachine, self).add_transition(trigger, source, dest, conditions=conditions, unless=unless,
  52. before=before, after=after, prepare=prepare, **kwargs)
  53. self._needs_update = True
  54. def add_states(self, states, on_enter=None, on_exit=None, ignore_invalid_triggers=None, **kwargs):
  55. super(MarkupMachine, self).add_states(states, on_enter=on_enter, on_exit=on_exit,
  56. ignore_invalid_triggers=ignore_invalid_triggers, **kwargs)
  57. self._needs_update = True
  58. def _convert_states_and_transitions(self, root):
  59. state = getattr(self, 'scoped', self)
  60. if state.initial:
  61. root['initial'] = state.initial
  62. if state == self and state.name:
  63. root['name'] = self.name[:-2]
  64. self._convert_transitions(root)
  65. self._convert_states(root)
  66. def _convert_states(self, root):
  67. key = 'states' if getattr(self, 'scoped', self) == self else 'children'
  68. root[key] = []
  69. for state_name, state in self.states.items():
  70. s_def = _convert(state, self.state_attributes, self.skip_references)
  71. if isinstance(state_name, Enum):
  72. s_def['name'] = state_name.name
  73. else:
  74. s_def['name'] = state_name
  75. if getattr(state, 'states', []):
  76. with self(state_name):
  77. self._convert_states_and_transitions(s_def)
  78. root[key].append(s_def)
  79. def _convert_transitions(self, root):
  80. root['transitions'] = []
  81. for event in self.events.values():
  82. if self._omit_auto_transitions(event):
  83. continue
  84. for transitions in event.transitions.items():
  85. for trans in transitions[1]:
  86. t_def = _convert(trans, self.transition_attributes, self.skip_references)
  87. t_def['trigger'] = event.name
  88. con = [x for x in (rep(f.func, self.skip_references) for f in trans.conditions
  89. if f.target) if x]
  90. unl = [x for x in (rep(f.func, self.skip_references) for f in trans.conditions
  91. if not f.target) if x]
  92. if con:
  93. t_def['conditions'] = con
  94. if unl:
  95. t_def['unless'] = unl
  96. root['transitions'].append(t_def)
  97. def _add_markup_model(self, markup):
  98. initial = markup.get('state', None)
  99. if markup['class-name'] == 'self':
  100. self.add_model(self, initial)
  101. else:
  102. mod_name, cls_name = markup['class-name'].rsplit('.', 1)
  103. cls = getattr(importlib.import_module(mod_name), cls_name)
  104. self.add_model(cls(), initial)
  105. def _convert_models(self):
  106. models = []
  107. for model in self.models:
  108. state = getattr(model, self.model_attribute)
  109. model_def = dict(state=state.name if isinstance(state, Enum) else state)
  110. model_def['name'] = model.name if hasattr(model, 'name') else str(id(model))
  111. model_def['class-name'] = 'self' if model == self else model.__module__ + "." + model.__class__.__name__
  112. models.append(model_def)
  113. return models
  114. def _omit_auto_transitions(self, event):
  115. return self.auto_transitions_markup is False and self._is_auto_transition(event)
  116. # auto transition events commonly a) start with the 'to_' prefix, followed by b) the state name
  117. # and c) contain a transition from each state to the target state (including the target)
  118. def _is_auto_transition(self, event):
  119. if event.name.startswith('to_') and len(event.transitions) == len(self.states):
  120. state_name = event.name[len('to_'):]
  121. try:
  122. _ = self.get_state(state_name)
  123. return True
  124. except ValueError:
  125. pass
  126. return False
  127. @classmethod
  128. def _identify_callback(self, name):
  129. callback_type, target = super(MarkupMachine, self)._identify_callback(name)
  130. if callback_type:
  131. self._needs_update = True
  132. return callback_type, target
  133. def rep(func, skip_references=False):
  134. """ Return a string representation for `func`. """
  135. if isinstance(func, string_types):
  136. return func
  137. if isinstance(func, numbers.Number):
  138. return str(func)
  139. if skip_references:
  140. return None
  141. try:
  142. return func.__name__
  143. except AttributeError:
  144. pass
  145. if isinstance(func, partial):
  146. return "%s(%s)" % (
  147. func.func.__name__,
  148. ", ".join(itertools.chain(
  149. (str(_) for _ in func.args),
  150. ("%s=%s" % (key, value)
  151. for key, value in iteritems(func.keywords if func.keywords else {})))))
  152. return str(func)
  153. def _convert(obj, attributes, skip):
  154. s = {}
  155. for key in attributes:
  156. val = getattr(obj, key, False)
  157. if not val:
  158. continue
  159. if isinstance(val, string_types):
  160. s[key] = val
  161. else:
  162. try:
  163. s[key] = [rep(v, skip) for v in iter(val)]
  164. except TypeError:
  165. s[key] = rep(val, skip)
  166. return s