Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/tutorius/tutorial.py
blob: 8970d4f195b85fc5376bd31ae64762a69b72a1b7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705

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"
    _INITIAL_TRANSITION_NAME = _INIT + "/transition0"
    _AUTOMATIC_TRANSITION_EVENT = "automatic"

    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 but it contains 
        a single automatic transition <Tutorial._INITIAL_TRANSITION_NAME>
        between the initial state <Tutorial._INIT> and the end state 
        <Tutorial._END>.

        The end state doesn't contain any action nor transition. 

        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
        @raise InvalidStateDictionary
        """
        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
        if not state_dict: 
            self._state_dict = \
                 {Tutorial._INIT:State(name=Tutorial._INIT),\
                  Tutorial._END:State(name=Tutorial._END)}
         
            self.add_transition(Tutorial._INIT, \
                 (Tutorial._AUTOMATIC_TRANSITION_EVENT, Tutorial._END))
        else:
            raise NotImplementedError("Tutorial: Initilization from a dictionary is not supported yet")

        # 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")

        # TODO: Validate once validation is working
        #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 actions are added using add_action.

        The transitions are added using add_transition.
        
        @param action_list The list of valid actions for this state
        @param transition_list The list of valid transitions
        @return 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)

        self._state_dict[name] = state

        return name
        

    def add_action(self, state_name, action):
        """
        Add an action to a specific state. A name unique throughout the 
        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
        @raise NameError if state_name doesn't exist
        """
        if not self._state_dict.has_key(state_name): 
            raise NameError("Tutorial: state <" + state_name +\
                            "> is not defined")

        self._validate_action(action)

        return self._state_dict[state_name].add_action(action)

    def add_transition(self, state_name, transition):
        """
        Add a transition to a specific state. A name unique throughout the
        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 NameError if state_name doesn't exist
        @raise TransitionAlreadyExists
        """
        if not self._state_dict.has_key(state_name): 
            raise NameError("Tutorial: state <" + state_name +\
                            "> is not defined")

        self._validate_transition(transition)

        # The unicity of the transition is validated by the state
        return self._state_dict[state_name].add_transition(transition)

    def update_action(self, action_name, new_action):
        """
        Replace the action with action_name by new_action 
        
        The action is validated.
 
        @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
        """
        state_name = action_name[:action_name.find("/")]

        if not self._state_dict.has_key(state_name): 
            raise NameError("Tutorial: action <" + action_name +\
                            "> is not defined")

        self._validate_action(new_action)

        return self._state_dict[state_name].update_action(action_name, new_action) 

    def update_transition(self, transition_name, new_transition):
        """
        Replace the transition with transition_name by new_transition 
        
        The transition is validated.
 
        @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
        """
        state_name = transition_name[:transition_name.find("/")]

        if not self._state_dict.has_key(state_name): 
            raise NameError("Tutorial: transition <" + transition_name +\
                            "> is not defined")

        self._validate_transition(new_transition)

        return self._state_dict[state_name].update_transition(transition_name, new_transition) 

    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
        @raise NameError if transition_name doesn't exist
        """
        state_name = action_name[:action_name.find("/")]

        if not self._state_dict.has_key(state_name): 
            raise NameError("Tutorial: action <" + action_name +\
                            "> is not defined")

        return self._state_dict[state_name].delete_action(action_name) 

    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
        @raise NameError if transition_name doesn't exist
        """
        state_name = transition_name[:transition_name.find("/")]

        if not self._state_dict.has_key(state_name): 
            raise NameError("Tutorial: transition <" + transition_name +\
                            "> is not defined")

        return self._state_dict[state_name].delete_transition(transition_name) 

    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 point to the next state and remove all the
        unreachable states recursively.

        All but the INIT and END states can be deleted.

        @param state_name The name of the state to remove
        @return The deleted state
        @raise StateDeletionError when trying to delete the INIT or the END state
        @raise NameError if state_name doesn't exist
        """
        if state_name == Tutorial._INIT or state_name == Tutorial._END:
            raise StateDeletionError("<" + state_name + "> cannot be deleted")

        
        if not self._state_dict.has_key(state_name): 
            raise NameError("Tutorial: state <" + transition_name +\
                            "> is not defined")

        next_states = set(self.get_following_states_dict(state_name).values())
        previous_states = set(self.get_previous_states_dict(state_name).values())

        # For now tutorials should be completely linear, 
        # let's make sure they are 
        assert len(next_states) <= 1 and len(previous_states) <= 1

        # Update transitions only if they existed
        if len(next_states) == 1 and len(previous_states) == 1:
            next_state = next_states.pop()
            previous_state = previous_states.pop()

            transitions = previous_state.get_transition_dict() 
            for transition_name, (event, state_to_delete) in \
                transitions.iteritems():
                self.update_transition(transition_name, (event, next_state.name))

        # Since we assume tutorials are linear for now, we do not need
        # to search for unreachable states

        return self._state_dict.pop(state_name)

        

    def get_action_dict(self, state_name=None):
        """
        Returns a dictionary of all actions for a specific state. 
        If no state_name is provided, returns an action dictionary 
        containing actions for all states.

        @param state_name The name of the state to list actions from  
        @return A dictionary of actions with action_name as key and action as value for state_name
        @raise NameError if state_name doesn't exist
        """
        if state_name and not self._state_dict.has_key(state_name):
            raise NameError("Tutorial: state <" + state_name +\
                            "> is not defined")
        elif state_name:
            return self._state_dict.get_action_dict()
        else:
            action_dict = {}
            for state in self._state_dict.itervalues():
                action_dict.update(state.get_action_dict())
            return action_dict

    def get_transition_dict(self, state_name=None):
        """
        Returns a dictionary of all actions for a specific state. 
        If no state_name is provided, returns an action dictionary 
        containing actions for all states.

        @param state_name The name of the state to list actions from  
        @return A dictionary of transitions with transition_name as key and transition as value for state_name
        @raise NameError if state_name doesn't exist
        """
        if state_name and not self._state_dict.has_key(state_name):
            raise NameError("Tutorial: state <" + state_name +\
                            "> is not defined")
        elif state_name:
            return self._state_dict.get_transition_dict()
        else:
            transition_dict = {}
            for state in self._state_dict.itervalues():
                transition_dict.update(state.get_transition_dict())
            return transition_dict


    def get_state_dict(self):
        """
        @return A dictionary of all the states in the tutorial with state_name as key and state as value
        """
        return self._state_dict

    def get_following_states_dict(self, state_name):
        """
        Returns a dictionary of the states that are immediately reachable from 
        a specific state.
        
        @param state_name The name of the state
        @raise NameError if state_name doesn't exist
        """
        if not self._state_dict.has_key(state_name):
            raise NameError("Tutorial: state <" + state_name +\
                            "> is not defined")
        
        following_states_dict = {}
        for (event, next_state) in \
            self._state_dict[state_name].get_transition_dict().itervalues():
            following_states_dict[next_state] = self._state_dict[next_state]

        return following_states_dict

    def get_previous_states_dict(self, state_name):
        """
        Returns a dictionary of the states that can transition to a 
        specific state.
        
        @param state_name The name of the state
        @raise NameError if state_name doesn't exist
        """
        if not self._state_dict.has_key(state_name):
            raise NameError("Tutorial: state <" + state_name +\
                            "> is not defined")
        

        previous_states_dict = {}
        for iter_state_name, state in \
            self._state_dict.iteritems():

            for (event, next_state) in \
                self._state_dict[iter_state_name].get_transition_dict().itervalues():

                if next_state != state_name:
                    continue

                previous_states_dict[iter_state_name] = state
                # if we have found one, do not look for other transitions
                # from this state
                break

        return previous_states_dict

    # Convenience methods for common tutorial manipulations
    def add_state_before(self, state_name, action_list=[], event_list=[]):
        """
        Add a new state just before another state state_name.  All transitions
        going to state_name are updated to end on the new state and all
        events will be converted to transitions ending on state_name.

        When event_list is empty, an automatic transition to state_name
        will be added to maintain consistency.

        @param state_name The name of the state that will be preceded by the
                          new state 
        @param action_list The list of valid actions for this state
        @param event_list The list of events that will be converted to transitions to state_name
        @return unique name for this state
        @raise NameError if state_name doesn't exist
        """
        raise NotImplementedError

    # Callback mecanism to allow automatic change notification when
    # the tutorial is modified
    def register_action_added_cb(self, cb):
        """
        Register a function cb that will be called when any action from 
        the tutorial is added.

        cb should be of the form:

        cb(action_name, new_action) where:
            action_name is the unique name of the action that was added
            new_action is the new action

        @param cb The callback function to be called
        @raise InvalidCallbackFunction if the callback has less or more than 
               2 arguments
        """
        raise NotImplementedError

    def register_action_updated_cb(self, cb):
        """
        Register a function cb that will be called when any action from 
        the tutorial is updated.

        cb should be of the form:

        cb(action_name, new_action) where:
            action_name is the unique name of the action that has changed
            new_action is the new action that replaces the old one

        @param cb The callback function to be called
        @raise InvalidCallbackFunction if the callback has less or more than 
               2 arguments
        """
        raise NotImplementedError
    
    def register_action_deleted_cb(self, cb):
        """
        Register a function cb that will be called when any action from 
        the tutorial is deleted.

        cb should be of the form:

        cb(action_name, old_action) where:
            action_name is the unique name of the action that was deleted
            old_action is the new action that replaces the old one

        @param cb The callback function to be called
        @raise InvalidCallbackFunction if the callback has less or more than 
               2 arguments
        """
        raise NotImplementedError

    def register_transition_updated_cb(self, cb):
        """
        Register a function cb that will be called when any transition from 
        the tutorial is updated.

        cb should be of the form:

        cb(transition_name, new_transition) where:
            transition_name is the unique name of the transition 
                            that has changed
            new_transition is the new transition that replaces the old one

        @param cb The callback function to be called
        @raise InvalidCallbackFunction if the callback has less or more than 
               2 arguments
        """
        raise NotImplementedError

    # Validation to assert precondition
    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_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(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.
        """
        raise NotImplementedError

    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 str(self._state_dict)

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

        # Initialize internal variables for name generation
        self.action_name_nb = 0
        self.transition_name_nb = 0
        
        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

        
    # 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 = self.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 = self.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


class InvalidStateDictionary(Exception):
    """
    Raised when an initialization dictionary could not be used to initialize
    a tutorial
    """
    pass

class StateDeletionError(Exception):
    """
    Raised when trying to delete an INIT or an END state from a tutorial
    """
    pass