def _validate_historystate_memory(self) -> bool: """ Checks that every *HistoryStateMixin*'s memory refer to one of its parent's children, except itself. :return: True :raise StatechartError: """ for name, state in self._states.items(): if isinstance(state, HistoryStateMixin): memory = state.memory if memory is None: continue if memory == name: raise StatechartError( 'Initial memory {} of {} cannot target itself'.format( state.memory, state)) if memory not in self._states: raise StatechartError( 'Initial memory {} of {} does not exist'.format( state.memory, state)) if state.memory not in self.children_for( self.parent_for(name)): raise StatechartError( 'Initial memory {} of {} must be a parent\'s child'. format(state.memory, state)) return True
def _import_state_from_dict(state_d: Mapping[str, Any]) -> StateMixin: """ Return the appropriate type of state from given dict. :param state_d: a dictionary containing state data :return: a specialized instance of State """ name = state_d.get('name') stype = state_d.get('type', None) on_entry = state_d.get('on entry', None) on_exit = state_d.get('on exit', None) if stype == 'final': state = FinalState(name, on_entry=on_entry, on_exit=on_exit) # type: Any elif stype == 'shallow history': state = ShallowHistoryState(name, on_entry=on_entry, on_exit=on_exit, memory=state_d.get('memory', None)) elif stype == 'deep history': state = DeepHistoryState(name, on_entry=on_entry, on_exit=on_exit, memory=state_d.get('memory', None)) elif stype is None: substates = state_d.get('states', None) parallel_substates = state_d.get('parallel states', None) if substates and parallel_substates: raise StatechartError( '{} cannot declare both a "states" and a "parallel states" property' .format(name)) elif substates: state = CompoundState(name, initial=state_d.get('initial', None), on_entry=on_entry, on_exit=on_exit) elif parallel_substates: state = OrthogonalState(name, on_entry=on_entry, on_exit=on_exit) else: state = BasicState(name, on_entry=on_entry, on_exit=on_exit) else: raise StatechartError('Unknown type {} for state {}'.format( stype, name)) # Preconditions, postconditions and invariants for condition in state_d.get('contract', []): if condition.get('before', None): state.preconditions.append(condition['before']) elif condition.get('after', None): state.postconditions.append(condition['after']) elif condition.get('always', None): state.invariants.append(condition['always']) return state
def import_from_dict(data: Mapping[str, Any]) -> Statechart: data = data['statechart'] statechart = Statechart(name=data['name'], description=data.get('description', None), preamble=data.get('preamble', None)) states = [] # (StateMixin instance, parent name) transitions = [] # Transition instances # (State dict, parent name) data_to_consider = [ (data['root state'], None) ] # type: List[Tuple[Mapping[str, Any], Optional[str]]] while data_to_consider: state_data, parent_name = data_to_consider.pop() # Get state try: state = _import_state_from_dict(state_data) except StatechartError: raise except Exception as e: raise StatechartError('Unable to load given YAML') from e states.append((state, parent_name)) # Get substates if isinstance(state, CompoundState): for substate_data in state_data['states']: data_to_consider.append((substate_data, state.name)) elif isinstance(state, OrthogonalState): for substate_data in state_data['parallel states']: data_to_consider.append((substate_data, state.name)) # Get transition(s) for transition_data in state_data.get('transitions', []): try: transition = _import_transition_from_dict( state.name, transition_data) except StatechartError: raise except Exception as e: raise StatechartError('Unable to load given YAML') from e transitions.append(transition) # Register on statechart for state, parent in states: statechart.add_state(state, parent) for transition in transitions: statechart.add_transition(transition) return statechart
def add_state(self, state: StateMixin, parent) -> None: """ Add given state (a *StateMixin* instance) on given parent (its name as an *str*). If given state should be use as a root state, set *parent* to None. :param state: state to add :param parent: name of its parent, or None :raise StatechartError: """ # Check name unicity if state.name in self._states.keys(): raise StatechartError('State {} already exists!'.format(state)) if not parent: # Check root state if self.root: raise StatechartError( 'Root already defined, {} should declare an existing parent state' .format(state)) else: parent_state = self.state_for(parent) # Check that parent exists if not parent_state: raise StatechartError( 'Parent "{}" of {} does not exist!'.format(parent, state)) # Check that parent is a CompositeStateMixin. if not isinstance(parent_state, CompositeStateMixin): raise StatechartError( '{} cannot be used as a parent for {}'.format( parent_state, state)) # If state is an HistoryState, its parent must be a CompoundState if isinstance(state, HistoryStateMixin) and not isinstance( parent_state, CompoundState): raise StatechartError( '{} cannot be used as a parent for {}'.format( parent_state, state)) # Save state self._states[state.name] = state self._parent[state.name] = parent self._children[state.name] = [] self._children[parent].append(state.name)
def _validate_compoundstate_initial(self) -> bool: """ Checks that every *CompoundState*'s initial state refer to one of its children :return: True :raise StatechartError: """ for name, state in self._states.items(): if isinstance(state, CompoundState) and state.initial: if state.initial not in self._states: raise StatechartError( 'Initial state {} of {} does not exist'.format( state.initial, state)) if state.initial not in self.children_for(name): raise StatechartError( 'Initial state {} of {} must be a child state'.format( state.initial, state)) return True
def remove_transition(self, transition: Transition) -> None: """ Remove given transitions. :param transition: a *Transition* instance :raise StatechartError: if transition is not registered """ try: self._transitions.remove(transition) except ValueError: raise StatechartError( 'Transition {} does not exist'.format(transition))
def rotate_transition(self, transition: Transition, new_source: str = '', new_target: Optional[str] = '') -> None: """ Rotate given transition. You MUST specify either *new_source* (a valid state name) or *new_target* (a valid state name or None) or both. :param transition: a *Transition* instance :param new_source: a state name :param new_target: a state name or None :raise StatechartError: if given transition or a given state does not exist. """ # Check that either new_source or new_target is set if new_source == new_target == '': raise ValueError( 'You must at least specify the new source or new target') # Check that transition exists if transition not in self._transitions: raise StatechartError('Unknown transition {}'.format(transition)) # Rotate using source if new_source != '': new_source_state = self.state_for(new_source) if not isinstance(new_source_state, TransitionStateMixin): raise StatechartError( '{} cannot have transitions'.format(new_source_state)) assert isinstance(new_source_state, StateMixin) transition._source = new_source_state.name # Rotate using target if new_target != '': if new_target is None: transition._target = None else: new_target_state = self.state_for(new_target) transition._target = new_target_state.name
def parent_for(self, name: str) -> str: """ Return the name of the parent of given state name. :param name: a state name :return: its parent name, or None. :raise StatechartError: if state does not exist """ try: return self._parent[name] except KeyError as e: raise StatechartError( 'State {} does not exist'.format(name)) from e
def state_for(self, name: str) -> StateMixin: """ Return the state instance that has given name. :param name: a state name :return: a *StateMixin* that has the same name or None :raise StatechartError: if state does not exist """ try: return self._states[name] except KeyError as e: raise StatechartError( 'State {} does not exist'.format(name)) from e
def rename_state(self, old_name: str, new_name: str) -> None: """ Change state name, and adapt transitions, initial state, memory, etc. :param old_name: old name of the state :param new_name: new name of the state """ if old_name == new_name: return if new_name in self._states: raise StatechartError('State {} already exists!'.format(new_name)) # Check state exists state = self.state_for(old_name) # Change transitions for transition in self.transitions: if transition.source == old_name: if transition.internal: transition._target = new_name transition._source = new_name if transition.target == old_name: transition._target = new_name for other_state in self._states.values(): # Change initial (CompoundState) if isinstance(other_state, CompoundState): if other_state.initial == old_name: other_state.initial = new_name # Change memory (HistoryState) if isinstance(other_state, HistoryStateMixin): if other_state.memory == old_name: other_state.memory = new_name # Adapt parent if self._parent[other_state.name] == old_name: self._parent[other_state.name] = new_name # Adapt structures parent_name = self._parent[old_name] self._children[parent_name].remove(old_name) self._children[parent_name].append(new_name) self._states[new_name] = self._states.pop(old_name) self._parent[new_name] = self._parent.pop(old_name) self._children[new_name] = self._children.pop(old_name) # Rename state! state._name = new_name
def add_transition(self, transition: Transition) -> None: """ Register given transition and register it on the source state :param transition: transition to add :raise StatechartError: """ # Check that source state is known if transition.source not in self._states: raise StatechartError( 'Unknown source state for {}'.format(transition)) from_state = self.state_for(transition.source) # Check that source state is a TransactionStateMixin if not isinstance(from_state, TransitionStateMixin): raise StatechartError('Cannot add {} on {}'.format( transition, from_state)) # Check either internal OR target state is known if transition.target is not None and transition.target not in self._states: raise StatechartError( 'Unknown target state for {}'.format(transition)) self._transitions.append(transition)
def move_state(self, name: str, new_parent: str) -> None: """ Move given state (and its children) such that its new parent is *new_parent*. Notice that a state cannot be moved inside itself or inside one of its descendants. If the state to move is the target of an *initial* or *memory* property of its parent, this property will be set to None. The same occurs if given state is an history state. :param name: name of the state to move :param new_parent: name of the new parent """ # Check that both states exist state = self.state_for(name) self.state_for(new_parent) # Check that parent is not a descendant (or self) of given state if new_parent in [name] + self.descendants_for(name): raise StatechartError( 'State {} cannot be moved into itself or one of its descendants.' .format(state)) # Change its parent and register state as a child old_parent = self.parent_for(name) self._parent[name] = new_parent self._children[old_parent].remove(name) self._children.setdefault(new_parent, []).append(name) # Check memory property if isinstance(state, HistoryStateMixin): state.memory = None for other_state in self._states.values(): # Change initial (CompoundState) if isinstance(other_state, CompoundState): if other_state.initial == name: other_state.initial = None # Change memory (HistoryState) if isinstance(other_state, HistoryStateMixin): if other_state.memory == name: other_state.memory = None
def state_for(name): if name == 'unknown state': raise StatechartError() else: return mocker.DEFAULT