From fd6570322dd03c5e1a3982814988b7ab878a5d13 Mon Sep 17 00:00:00 2001 From: Alexandre Decan Date: Mon, 26 Feb 2018 15:46:01 +0100 Subject: [PATCH] Export to plantuml reworked --- sismic/io/plantuml.py | 264 ++++++++++++++++++++++-------------------- 1 file changed, 139 insertions(+), 125 deletions(-) diff --git a/sismic/io/plantuml.py b/sismic/io/plantuml.py index 8946b08..3b676c7 100644 --- a/sismic/io/plantuml.py +++ b/sismic/io/plantuml.py @@ -10,25 +10,26 @@ class _Exporter: def __init__( self, - statechart: Statechart, + statechart: Statechart, *, statechart_name=True, statechart_description=True, statechart_preamble=True, state_contracts=True, state_actions=True, - transition_action=True, - ): + transition_contracts=True, + transition_action=True): self.statechart = statechart self.statechart_name = statechart_name self.statechart_description = statechart_description self.statechart_preamble = statechart_preamble self.state_contracts = state_contracts self.state_actions = state_actions + self.transition_contracts = transition_contracts self.transition_action = transition_action + self._states_id = {} self._output = [] self._indent = 0 - self._note_nb = 0 def indent(self): self._indent += 2 @@ -40,6 +41,9 @@ def concat(self, text, *, wrap=''): lines = text.strip().split('\n') for line in lines: + # Special case for __old__ + line = line.replace('__old__', '~__old__') + self._output.append( '{indent}{wrap}{line}{wrap}'.format( indent=' ' * self._indent, @@ -49,144 +53,152 @@ def concat(self, text, *, wrap=''): ) def state_id(self, name): - return name.replace(' ', '') + return ''.join(filter(str.isalnum, name)) def export_statechart(self): - if self.statechart_name or self.statechart_description or self.statechart_preamble: - self.concat('note top of {}'.format(self.state_id(self.statechart.root))) + if self.statechart_name and self.statechart.name: + self.concat('title {}'.format(self.statechart.name)) - self.indent() - if self.statechart_name and self.statechart.name: - self.concat(self.statechart.name, wrap='**') - if self.statechart_description and self.statechart.description: - self.concat(self.statechart.description, wrap='//') - if self.statechart_preamble and self.statechart.preamble: - self.concat(self.statechart.preamble) + if self.statechart_description and self.statechart.description: + self.concat('caption {}'.format(self.statechart.description.replace('\n', '\\n'))) + if self.statechart_preamble and self.statechart.preamble: + self.concat('note top of {}'.format(self.state_id(self.statechart.root))) + self.indent() + self.concat(self.statechart.preamble) self.deindent() - self.concat('end note') def export_state(self, name: str): state = self.statechart.state_for(name) - self.export_state_actions(name) - self.export_state_contracts(name) + # Special case for final state + if isinstance(state, FinalState): + # Find transitions leading to it + for transition in self.statechart.transitions_to(state.name): + self.export_transition(transition) + return - if isinstance(state, BasicState): - self.concat('state "{}" as {}'.format(name, self.state_id(name))) - elif isinstance(state, CompoundState): + if isinstance(state, ShallowHistoryState): + self.concat('state "H" as {} {{'.format(self.state_id(name))) + elif isinstance(state, DeepHistoryState): + self.concat('state "H*" as {} {{'.format(self.state_id(name))) + else: self.concat('state "{}" as {} {{'.format(name, self.state_id(name))) - self.indent() - for child in self.statechart.children_for(name): - self.export_state(child) - if state.initial: - self.concat('[*] --> {}'.format(self.state_id(state.initial))) + self.indent() - self.deindent() - self.concat('}') - elif isinstance(state, OrthogonalState): - self.concat('state "{}" as {} {{'.format(name, self.state_id(name))) - self.indent() + # Actions + has_actions = False - for i, child in enumerate(self.statechart.children_for(name)): - if i != 0: - self.concat('--') - self.export_state(child) + if self.state_actions and isinstance(state, ActionStateMixin): + # On entry, on exit + if state.on_entry: + has_actions = True + self.concat('{} : **entry**:\\n{}'.format( + self.state_id(name), + state.on_entry.strip().replace('\n', '\\n') + )) + if state.on_exit: + has_actions = True + self.concat('{} : **exit**:\\n{}'.format( + self.state_id(name), + state.on_exit.strip().replace('\n', '\\n') + )) - self.deindent() - self.concat('}') + # Internal actions + transitions = [tr for tr in self.statechart.transitions_from(name) if tr.internal and tr.action] + if len(transitions) > 0: + has_actions = True + for transition in transitions: + text = [] + if transition.event: + text.append('**{}**'.format(transition.event)) + if transition.guard: + text.append('[{}]'.format(transition.guard)) + + self.concat('{} : {}:\\n{}'.format( + self.state_id(name), + ''.join(text), + transition.action.strip().replace('\n', '\\n'), + )) + + # Contracts + if isinstance(state, ContractMixin) and self.state_contracts and ( + state.preconditions or state.invariants or state.postconditions): + if has_actions: + self.concat('{} : '.format(self.state_id(name))) - elif isinstance(state, FinalState): - pass - elif isinstance(state, ShallowHistoryState): - self.concat('state "H - {}" as {}'.format(name, self.state_id(name))) - elif isinstance(state, DeepHistoryState): - self.concat('state "H* - {}" as {}'.format(name, self.state_id(name))) - else: - assert False, type(state) + for cond in state.preconditions: + self.concat('{} : **pre:** {}'.format(self.state_id(name), cond)) + for cond in state.invariants: + self.concat('{} : **inv:** {}'.format(self.state_id(name), cond)) + for cond in state.postconditions: + self.concat('{} : **post:** {}'.format(self.state_id(name), cond)) - def export_state_actions(self, name): - state = self.statechart.state_for(name) + # Nested states + for i, child in enumerate(self.statechart.children_for(name)): + if i != 0 and isinstance(state, OrthogonalState): + self.concat('--') + self.export_state(child) - if not self.state_actions or not isinstance(state, ActionStateMixin): - return + # Initial state + if hasattr(state, 'initial') and state.initial: + self.concat('[*] --> {}'.format(self.state_id(state.initial))) - # On entry, on exit - if state.on_entry: - self.concat('{} : **on entry**:\\n{}'.format( - self.state_id(name), - state.on_entry.strip().replace('\n', '\\n') - )) - if state.on_exit: - self.concat('{} : **on exit**:\\n{}'.format( - self.state_id(name), - state.on_exit.strip().replace('\n', '\\n') - )) - - # Internal actions - transitions = [tr for tr in self.statechart.transitions_from(name) if tr.internal and tr.action] - if len(transitions) > 0: - for transition in transitions: - text = [] - if transition.event: - text.append('on event {}'.format(transition.event)) - if transition.guard: - text.append('[{}]'.format(transition.guard)) - - self.concat('{} : **{}**:\\n{}'.format( - self.state_id(name), - ''.join(text), - transition.action.strip().replace('\n', '\\n'), - )) + self.deindent() + self.concat('}') - def export_transition(self, source_name): + def export_transitions(self, source_name): state = self.statechart.state_for(source_name) - transitions = self.statechart.transitions_from(source_name) + + # History state + if isinstance(state, HistoryStateMixin) and state.memory: + self.concat('{} --> {}'.format(self.state_id(source_name), self.state_id(state.memory))) + + # Transitions (except internal ones) + transitions = filter(lambda t: not t.internal, self.statechart.transitions_from(source_name)) for transition in transitions: - if transition.internal: + # Do not treat final states here + if transition.target and isinstance(self.statechart.state_for(transition.target), FinalState): continue - target = self.statechart.state_for(transition.target) - if isinstance(target, FinalState): - target_name = '[*]' - else: - target_name = self.state_id(target.name) - - text = [] - if transition.event: - text.append(transition.event) - if transition.guard: - text.append('[{}]'.format(transition.guard)) - if transition.action and self.transition_action: - text.append(' / {}'.format(transition.action.replace('\n', '; '))) - - self.concat('{source} --> {target} : {text}'.format( - source=self.state_id(source_name), - target=target_name, - text=''.join(text), - )) - - def export_state_contracts(self, name): - state = self.statechart.state_for(name) + self.export_transition(transition) - if isinstance(state, ContractMixin) and self.state_contracts and ( - state.preconditions or state.invariants or state.postconditions): - #self._note_nb += 1 - self.concat('note bottom of {}'.format(self.state_id(name))) - # self.concat('note as N{}'.format(self._note_nb)) + def export_transition(self, transition): + target = self.statechart.state_for(transition.target) + + if isinstance(target, FinalState): + target_name = '[*]' + else: + target_name = self.state_id(target.name) + + text = [] + if transition.event: + text.append(transition.event) + if transition.guard: + text.append('[{}]'.format(transition.guard)) + if transition.action and self.transition_action: + text.append(' / {}'.format(transition.action.replace('\n', '; '))) + + if self.transition_contracts and ( + transition.preconditions or transition.invariants or transition.postconditions): + text.append('\\n') + + for cond in transition.preconditions: + text.append('pre: {}\\n'.format(cond)) + for cond in transition.invariants: + text.append('inv: {}\n'.format(cond)) + for cond in transition.postconditions: + text.append('post: {}\n'.format(cond)) + + self.concat('{source} --> {target} : {text}'.format( + source=self.state_id(transition.source), + target=target_name, + text=''.join(text), + )) - self.indent() - for cond in state.preconditions: - self.concat('pre: {}'.format(cond)) - for cond in state.invariants: - self.concat('inv: {}'.format(cond)) - for cond in state.postconditions: - self.concat('post: {}'.format(cond)) - self.deindent() - self.concat('end note') def export_all(self): self.concat('@startuml') @@ -195,7 +207,7 @@ def export_all(self): self.export_state(self.statechart.root) for name in self.statechart.states: - self.export_transition(name) + self.export_transitions(name) self.concat('@enduml') @@ -209,8 +221,8 @@ def export_to_plantuml( statechart_preamble=True, state_contracts=True, state_actions=True, - transition_action=True - ): + transition_contracts=True, + transition_action=True) -> str: """ Export given statechart to plantUML (see http://plantuml/plantuml). @@ -218,20 +230,22 @@ def export_to_plantuml( :param statechart_name: include the name of the statechart :param statechart_description: include the description of the statechart :param statechart_preamble: include the preamble of the statechart - :param state_contracts: include a note containing state contracts + :param state_contracts: include state contracts :param state_actions: include state actions (on entry, on exit and internal transitions) + :param transition_contracts: include transition contracts :param transition_action: include actions on transition - :return: + :return: textual representation using plantuml """ exporter = _Exporter( statechart, - statechart_name, - statechart_description, - statechart_preamble, - state_contracts, - state_actions, - transition_action + statechart_name=statechart_name, + statechart_description=statechart_description, + statechart_preamble=statechart_preamble, + state_contracts=state_contracts, + state_actions=state_actions, + transition_contracts=transition_contracts, + transition_action=transition_action, ) return exporter.export_all() \ No newline at end of file