diff options
Diffstat (limited to 'tutorius/tutorial.py')
-rw-r--r-- | tutorius/tutorial.py | 467 |
1 files changed, 467 insertions, 0 deletions
diff --git a/tutorius/tutorial.py b/tutorius/tutorial.py new file mode 100644 index 0000000..a4da092 --- /dev/null +++ b/tutorius/tutorial.py @@ -0,0 +1,467 @@ + +class Tutorial(object): + """ This class replaces the previous Tutorial class and + allows manipulation of the abstract representation + of a tutorial as a state machine + """ + + _INIT = "INIT" + _END = "END" + + def __init__(self, name, state_dict=None): + """ + The constructor for the Tutorial. By default, the tutorial contains + only an initial state and an end state. + The initial state doesn't contain any action or transition. + The end state doesn't contain any action either. + + If state_dict is provided, a valid initial state and an end state + must be provided. + + @param name The name of the tutorial + @param state_dict optional, a valid dictionary of states + """ + self.name = name + + # We will use an adjacency list representation through the + # usage of state objects because our graph representation + # is really sparse and mostly linear, for a brief + # example of graph programming in python see: + # http://www.python.org/doc/essays/graphs + self._state_dict = state_dict or \ + {Tutorial._INIT:State(name=Tutorial._INIT),\ + Tutorial._END:State(name=Tutorial._END)} + + # Minimally check for the presence of an INIT and an END + # state + if not self._state_dict.has_key(Tutorial._INIT): + raise Exception("No INIT state found in state_dict") + + if not self._state_dict.has_key(Tutorial._END): + raise Exception("No END state found in state_dict") + + self.validate() + + # Initialize variables for generating unique names + # TODO: We should take the max number from the + # existing state names + self._state_name_nb = 0 + + + def add_state(self, action_list=[], transition_list=[]): + """ + Add a new state to the state machine. The state is + initialized with the action list and transition list + and a new unique name is returned for this state. + + The action is added using add_action. + + The transitions is added using add_transition. + + @param action_list The list of valid actions for this state + @param transition_list The list of valid transitions + @return string unique name for this state + """ + name = self._generate_unique_state_name() + + for action in action_list: + self._validate_action(action) + + for transition in transition_list: + self._validate_transition(transition) + + state = State(name, action_list, transition_list) + + if self._state_dict.has_key(name): + raise Exception("Name: " + name + " already exists, could not\ + add a new state.") + + self._state_dict[name] = state + + return name + + + def add_action(self, state_name, action): + """ + Add an action to a specific state. A unique name for this + tutorial is generated to refer precisely to this action + and is returned. + + The action is validated. + + @param state_name The name of the state to add an action to + @param action The action to be added + @return unique name for this action + """ + return "State/Action" + + def add_transition(self, state_name, transition): + """ + Add a transition to a specific state. A unique name for this + tutorial is generated to refer precisely to this transition + and is returned. Inserting a duplicate transition will raise + an exception. + + The transition is validated. + + @param state_name The name of the state to add a transition to + @param transition The transition to be added + @return unique name for this action + @raise TransitionAlreadyExists + """ + return "State/Transition" + + def update_action(self, action_name, action): + """ + Update the properties of a specific action with a copy of the + properties of the action passed in. + + The action is validated. + + @param action_name The name of the action to update + @param action An action with the properties to copy from + @return action_name if the update was successful, False otherwise + """ + return action_name + + def update_transition(self, transition_name, transition): + """ + Update the properties of a specific transition with a copy of the + properties of the transition passed in. + + The transition is validated. + + @param transition_name The name of the transition to update + @param transition An transition with the properties to copy from + @return transition_name if the update was successful, False otherwise + """ + return transition_name + + def delete_action(self, action_name): + """ + Delete the action identified by action_name. + + @param action_name The name of the action to be deleted + @return the action that has been deleted + """ + return None + + def delete_transition(self, transition_name): + """ + Delete the transition identified by transition_name. + + @param transition_name The name of the transition to be deleted + @return the transition that has been deleted + """ + return None + + def delete_state(self, state_name): + """ + Delete the state, delete all the actions and transitions + in this state, update the transitions from the state that + pointed to this one to the next state and remove all the + unreachable states recursively. + + @param state_name The name of the state to remove + """ + pass + + def get_actions(self, state_name): + """ + @param state_name The name of the state to list actions from + @return A list of actions for state_name + """ + pass + + def get_events(self, state_name): + """ + @param state_name The name of the state to list actions from + @return A list of events for state_name + """ + pass + + def get_following_states(self, state_name): + """ + Returns a tuple of the names of the states that point to the given + state. If there is no such state, the function raises a KeyError. + + @param state_name The name of the state to analyse + @raise KeyError When there is no state by this name in the FSM + """ + pass + + + def get_previous_states(self, state_name): + """ + Returns a tuple of the names of the state that can transition to + the given state. If there is no such state, the function raises a + KeyError. + + @param state_name The name of the state that the returned states might + transition to. + @raise KeyError When there is no state by this name in the FSM + """ + pass + + def _validate_action(self, action): + """ + Validate that an action conforms to what we expect, + throws an exception otherwise. + + @param action The action to validate + @except InvalidAction if the action fails to conform to what we expect + """ + pass + + def _validate_action_name(self, action_name): + """ + Check if action_name exists. + + @param action_name The name to check + @except UnknownName if the name is not present in the Tutorial + """ + pass + + def _validate_transition(self, transition): + """ + Validate that a transition conforms to what we expect, + throws an exception otherwise. + + @param transition The transition to validate + @except InvalidTransition if the transition fails to conform to what we expect + """ + pass + + def _validate_transition_name(self, transition_name): + """ + Check if transition_name exists. + + @param transition_name The name to check + @except UnknownName if the name is not present in the Tutorial + """ + pass + + def validate(self): + """ + Validate the state machine for a serie of properties: + 1. No unreachable states + 2. No dead end state (except END) + 3. No branching in the main path + 4. No loop in the main path + 5. ... + + Throw an exception for the first condition that is not met. + """ + pass + + def _generate_unique_state_name(self): + name = "State" + str(self._state_name_nb) + self._state_name_nb += 1 + return name + + def __str__(self): + """ + Return a string representation of the tutorial + """ + return "" + +class State(object): + """ + This is a step in a tutorial. The state represents a collection of actions + to undertake when entering the state, and a series of transitions to lead + to next states. + + This class is not meant to be used explicitly as no validation is done on + inputs, the validation should be done by the containing class. + """ + + def __init__(self, name="", action_list=[], transition_list=[]): + """ + Initializes the content of the state, such as loading the actions + that are required and building the correct transitions. + + @param action_list The list of actions to execute when entering this + state + @param transition_list A list of tuples of the form + (event, next_state_name), that explains the outgoing links for + this state + """ + object.__init__(self) + + self.name = name + + self._actions = {} + for action in action_list: + self._actions[self._generate_unique_action_name(action)] = action + + self._transitions = {} + for transition in transition_list: + self._transitions[self._generate_unique_transition_name(transition)] = transition + + # Initialize internal variables for name generation + self.action_name_nb = 0 + self.transition_name_nb = 0 + + # Action manipulations + def add_action(self, new_action): + """ + Adds an action to the state + + @param new_action The action to add + @return a unique name for this action + """ + action_name = self._generate_unique_action_name(new_action) + self._actions[action_name] = new_action + return action_name + + def delete_action(self, action_name): + """ + Delete the action with the name action_name + + @param action_name The name of the action to delete + @return The action deleted + @raise NameError if action_name doesn't exist + """ + if self._actions.has_key(action_name): + return self._actions.pop(action_name) + else: + raise NameError("Tutorial.State: action '" + action_name + "' is not defined") + + def update_action(self, action_name, new_action): + """ + Replace the action with action_name by new_action + + @param action_name The name of the action to replace + @param new_action The action that will replace the old one + @return The replaced action + @raise NameError if action_name doesn't exist + """ + # TODO: For now let's just replace the action with a new one, + # we should check to see if we need a replace or an update + # semantic for this update method + if self._actions.has_key(action_name): + old_action = self._actions.pop(action_name) + self._actions[action_name] = new_action + return old_action + else: + raise NameError("Tutorial.State: action '" + action_name + "' is not defined") + + def get_action_dict(self): + """ + @return A dictionary of actions that the state will execute + """ + return self._actions + + def delete_actions(self): + """ + Removes all the action associated with this state. A cleared state will + not do anything when entered or exited. + """ + self._actions = {} + + # Transition manipulations + def add_transition(self, new_transition): + """ + Adds a transition from this state to another state. + + The same transition may not be added twice. + + @param transition The new transition. + @return A unique name for the transition + @raise TransitionAlreadyExists if an equivalent transition exists + """ + for transition in self._transitions.itervalues(): + if transition == new_transition: + raise TransitionAlreadyExists(str(transition)) + + transition_name = self._generate_unique_transition_name(new_transition) + self._transitions[transition_name] = new_transition + return transition_name + + def update_transition(self, transition_name, new_transition): + """ + Replace the transition with transition_name by new_transition + + @param transition_name The name of the transition to replace + @param new_transition The transition that will replace the old one + @return The replaced transition + @raise NameError if transition_name doesn't exist + """ + # TODO: For now let's just replace the transition with a new one, + # we should check to see if we need a replace or an update + # semantic for this update method + if self._transitions.has_key(transition_name): + old_transition = self._transitions.pop(transition_name) + self._transitions[transition_name] = new_transition + return old_transition + else: + raise NameError("Tutorial.State: transition '" + transition_name + "' is not defined") + + def delete_transition(self, transition_name): + """ + Delete the transition with the name transition_name + + @param transition_name The name of the transition to delete + @return The transition deleted + @raise NameError if transition_name doesn't exist + """ + if self._transitions.has_key(transition_name): + return self._transitions.pop(transition_name) + else: + raise NameError("Tutorial.State: transition '" + transition_name + "' is not defined") + + def get_transition_dict(self): + """ + @return The dictionary of transitions associated with this state. + """ + return self._transitions + + def delete_transitions(self): + """ + Delete all the transitions associated with this state. + """ + self._transitions = {} + + def _generate_unique_action_name(self, action): + """ + Returns a unique name for the action in this state, + the actual content of the name should not be relied upon + for correct behavior + + @param action The action to generate a name for + @return A name garanteed to be unique within this state + """ + #TODO use the action class name to generate a name + # to make it easier to debug and know what we are + # manipulating + name = "action" + str(self.action_name_nb) + self.action_name_nb += 1 + return name + + def _generate_unique_transition_name(self, transition): + """ + Returns a unique name for the transition in this state, + the actual content of the name should not be relied upon + for correct behavior + + @param transition The transition to generate a name for + @return A name garanteed to be unique within this state + """ + #TODO use the event class name from the transition to + # generate a name to make it easier to debug and know + # what we are manipulating + name = "transition" + str(self.transition_name_nb) + self.transition_name_nb += 1 + return name + + + +################## Error Handling and Exceptions ############################## + +class TransitionAlreadyExists(Exception): + """ + Raised when a duplicate transition is added to a state + """ + pass + + |