예제 #1
0
    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
예제 #2
0
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
예제 #3
0
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
예제 #4
0
    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)
예제 #5
0
    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
예제 #6
0
    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))
예제 #7
0
    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
예제 #8
0
    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
예제 #9
0
    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
예제 #10
0
    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
예제 #11
0
    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)
예제 #12
0
    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
예제 #13
0
 def state_for(name):
     if name == 'unknown state':
         raise StatechartError()
     else:
         return mocker.DEFAULT