diff options
author | mike <michael.jmontcalm@gmail.com> | 2009-11-16 01:22:33 (GMT) |
---|---|---|
committer | mike <michael.jmontcalm@gmail.com> | 2009-11-16 01:22:33 (GMT) |
commit | 66ab5d4ae50a92c11dd5ffec1d7ce5722f06b43a (patch) | |
tree | 90e79e149777e6cc4422ed8eac4e46f6092cf8d9 | |
parent | bea085a5f9ad96d3d9dad0dd8f71b5e57e7c64ab (diff) |
Updating Engine, Vault and Tutorial to associate names with actions
-rw-r--r-- | tests/probetests.py | 66 | ||||
-rw-r--r-- | tutorius/TProbe.py | 43 | ||||
-rw-r--r-- | tutorius/core.py | 640 | ||||
-rw-r--r-- | tutorius/engine.py | 31 | ||||
-rw-r--r-- | tutorius/translator.py | 15 | ||||
-rw-r--r-- | tutorius/tutorial.py | 8 | ||||
-rw-r--r-- | tutorius/vault.py | 4 |
7 files changed, 100 insertions, 707 deletions
diff --git a/tests/probetests.py b/tests/probetests.py index 65782a3..0667d00 100644 --- a/tests/probetests.py +++ b/tests/probetests.py @@ -20,6 +20,7 @@ Probe Tests import unittest import pickle +import functools from dbus.mainloop.glib import DBusGMainLoop from dbus.mainloop import NULL_MAIN_LOOP @@ -47,7 +48,7 @@ class MockAddon(Action): i = TIntProperty(0) s = TStringProperty("test") - def do(self): + def do(self, activity=None): global message_box message_box = (self.i, self.s) @@ -85,28 +86,37 @@ class MockProbeProxy(object): @param activityName unique activity id. Must be a valid dbus bus name. """ self.MockAction = None + self.MockActionAddress = None self.MockActionUpdate = None self.MockEvent = None self.MockCB = None self.MockAlive = True self.MockEventAddr = None + + self._address_nb = 0 def isAlive(self): return self.MockAlive - def install(self, action, block=False): + def install(self, action, callback, block=False): self.MockAction = action + action_name = 'new_action' + str(self._address_nb) + self._address_nb = self._address_nb + 1 + callback(action_name) + self.MockActionUpdate = None return None - def update(self, action, newaction, block=False): - self.MockAction = action + def update(self, action_address, newaction, block=False): + self.MockActionAddress = action_address self.MockActionUpdate = newaction return None - def uninstall(self, action, block=False): - self.MockAction = None - self.MockActionUpdate = None + def uninstall(self, action_address, block=False): + if self.MockActionAddress == action_address: + self.MockActionAddress = None + self.MockAction = None + self.MockActionUpdate = None return None def subscribe(self, event, callback, block=True): @@ -124,11 +134,13 @@ class MockProbeProxy(object): def detach(self, block=False): self.MockAction = None + self.MockActionAddress = None self.MockActionUpdate = None self.MockEvent = None self.MockCB = None self.MockAlive = False self.MockEventAddr = None + self._address_nb = 0 return None class MockProxyObject(object): @@ -183,6 +195,9 @@ class ProbeTest(unittest.TestCase): self.activity = MockActivity() self.probe = TProbe(self.activity, MockServiceProxy()) + # Assigned addresses for the registered actions + self._registered_actions = {} + #Override the eventOccured on the Probe... self.old_eO = self.probe.eventOccured def newEo(event): @@ -223,12 +238,16 @@ class ProbeTest(unittest.TestCase): address = self.probe.install(pickle.dumps(action)) assert type(address) == str, "install should return a string" assert message_box == (5, "woot"), "message box should have (i, s)" + #assert self._registered_actions['action1'] == 'new_action0', "Callback should give back the address" + #address = self._registered_actions['action1'] #install 2 action.i, action.s = (10, "ahhah!") address2 = self.probe.install(pickle.dumps(action)) assert message_box == (10, "ahhah!"), "message box should have changed" assert address != address2, "action addresses should be different" + #assert self._registered_actions['action2'] == 'new_action1', "Callback should give back the address" + #address2 = self._registered_actions['action2'] #uninstall 2 self.probe.uninstall(address2) @@ -297,6 +316,7 @@ class ProbeManagerTest(unittest.TestCase): def setUp(self): MockProbeProxy._MockProxyCache = {} self.probeManager = ProbeManager(proxy_class=MockProbeProxy) + self._registered_actions = {} def test_register_probe(self): assert len(self.probeManager.get_registered_probes_list()) == 0 @@ -336,6 +356,9 @@ class ProbeManagerTest(unittest.TestCase): assert len(self.probeManager.get_registered_probes_list("act1")) == 0 assert self.probeManager.get_registered_probes_list("act1") == [] + def _register_action(self, action_name, action_address): + self._registered_actions[action_name] = action_address + def test_actions(self): self.probeManager.register_probe("act1", "unique_id_1") self.probeManager.register_probe("act2", "unique_id_2") @@ -345,27 +368,28 @@ class ProbeManagerTest(unittest.TestCase): ad1 = MockAddon() #ErrorCase: install, update, uninstall without currentActivity #Action functions should do a warning if there is no activity - self.assertRaises(RuntimeWarning, self.probeManager.install, ad1) - self.assertRaises(RuntimeWarning, self.probeManager.update, ad1, ad1) - self.assertRaises(RuntimeWarning, self.probeManager.uninstall, ad1) + self.assertRaises(RuntimeWarning, self.probeManager.install, ad1, functools.partial(self._register_action, "action1")) + self.assertRaises(RuntimeWarning, self.probeManager.update, "No Name", ad1) + self.assertRaises(RuntimeWarning, self.probeManager.uninstall, "No Name") assert act1.MockAction is None, "Action should not be installed on inactive proxy" assert act2.MockAction is None, "Action should not be installed on inactive proxy" self.probeManager.currentActivity = "act1" - self.probeManager.install(ad1) + self.probeManager.install(ad1, functools.partial(self._register_action, "action1")) assert act1.MockAction == ad1, "Action should have been installed" + assert self._registered_actions["action1"] == 'new_action0', "Address for the action should have been registered" assert act2.MockAction is None, "Action should not be installed on inactive proxy" - self.probeManager.update(ad1, ad1) + self.probeManager.update(self._registered_actions["action1"], ad1) assert act1.MockActionUpdate == ad1, "Action should have been updated" assert act2.MockActionUpdate is None, "Should not update on inactive" self.probeManager.currentActivity = "act2" - self.probeManager.uninstall(ad1) + self.probeManager.uninstall(self._registered_actions["action1"]) assert act1.MockAction == ad1, "Action should still be installed" self.probeManager.currentActivity = "act1" - self.probeManager.uninstall(ad1) + self.probeManager.uninstall(self._registered_actions["action1"]) assert act1.MockAction is None, "Action should be uninstalled" def test_events(self): @@ -406,6 +430,8 @@ class ProbeProxyTest(unittest.TestCase): self.mockObj = MockProxyObject("unittest.TestCase", "/tutorius/Probe/unique_id_1") self.probeProxy = ProbeProxy("unittest.TestCase", "unique_id_1") + self._registered_actions = {} + def tearDown(self): dbus.SessionBus = old_SessionBus MockProxyObject._MockProxyObjects = {} @@ -417,6 +443,9 @@ class ProbeProxyTest(unittest.TestCase): self.mockObj.MockRet["ping"] = "anything else" assert self.probeProxy.isAlive() == False, "Alive should return False" + def _register_action(self, action_name, action_address): + self._registered_actions[action_name] = action_address + def test_actions(self): action = MockAddon() action.i, action.s = 5, "action" @@ -427,24 +456,25 @@ class ProbeProxyTest(unittest.TestCase): address = "Addr1" #Set the return value of probe install self.mockObj.MockRet["install"] = address - self.probeProxy.install(action, block=True) + callback = functools.partial(self._register_action, "action1") + self.probeProxy.install(action, callback, block=True) assert pickle.loads(self.mockObj.MockCall["install"]["args"][0]) == action, "1 argument, the action" #ErrorCase: Update should fail on noninstalled actions self.assertRaises(RuntimeWarning, self.probeProxy.update, action2, action2, block=True) #Test the update - self.probeProxy.update(action, action2, block=True) + self.probeProxy.update(address, action2, block=True) args = self.mockObj.MockCall["update"]["args"] assert args[0] == address, "arg 1 should be the action address" assert pickle.loads(args[1]) == action2._props, "arg2 should be the new action properties" #ErrorCase: Uninstall on not installed action (silent fail) #Test the uninstall - self.probeProxy.uninstall(action2, block=True) + self.probeProxy.uninstall("wrong address", block=True) assert not "uninstall" in self.mockObj.MockCall, "Uninstall should not be called if action is not installed" - self.probeProxy.uninstall(action, block=True) + self.probeProxy.uninstall(address, block=True) assert self.mockObj.MockCall["uninstall"]["args"][0] == address, "1 argument, the action address" def test_events(self): diff --git a/tutorius/TProbe.py b/tutorius/TProbe.py index 57de868..cfa734b 100644 --- a/tutorius/TProbe.py +++ b/tutorius/TProbe.py @@ -297,46 +297,47 @@ class ProbeProxy: except: return False - def __update_action(self, action, address): + def __update_action(self, action, callback, address): LOGGER.debug("ProbeProxy :: Updating action %s with address %s", str(action), str(address)) - self._actions[action] = str(address) + callback(address) - def __clear_action(self, action): - self._actions.pop(action, None) + def __clear_action(self, action_address): + self._actions.pop(action_address, None) - def install(self, action, block=False): + def install(self, action, callback, block=False): """ Install an action on the TProbe's activity @param action Action to install + @param callback The function to call to propagate the address @param block Force a synchroneous dbus call if True @return None """ return remote_call(self._probe.install, (pickle.dumps(action),), - save_args(self.__update_action, action), + save_args(self.__update_action, action, callback), block=block) - def update(self, action, newaction, block=False): + def update(self, action_address, newaction, block=False): """ Update an already installed action's properties and run it again - @param action Action to update + @param action_name The name of the action to update @param newaction Action to update it with @param block Force a synchroneous dbus call if True @return None """ #TODO review how to make this work well - if not action in self._actions: + if not action_address in self._actions.keys(): raise RuntimeWarning("Action not installed") #TODO Check error handling - return remote_call(self._probe.update, (self._actions[action], pickle.dumps(newaction._props)), block=block) + return remote_call(self._probe.update, (self._actions[action_address], pickle.dumps(newaction._props)), block=block) - def uninstall(self, action, block=False): + def uninstall(self, action_address, block=False): """ Uninstall an installed action - @param action Action to uninstall + @param action_name The name of the action to uninstall @param block Force a synchroneous dbus call if True """ - if action in self._actions: - remote_call(self._probe.uninstall,(self._actions.pop(action),), block=block) + if action_name in self._actions.keys(): + remote_call(self._probe.uninstall,(self._actions.pop(action_name),), block=block) def __update_event(self, event, callback, address): LOGGER.debug("ProbeProxy :: Registered event %s with address %s", str(hash(event)), str(address)) @@ -400,7 +401,7 @@ class ProbeProxy: # TODO elavoie 2009-07-25 When we will allow for patterns both # for event types and sources, we will need to revise the lookup - # mecanism for which callback function to call + # mechanism for which callback function to call return remote_call(self._probe.subscribe, (pickle.dumps(event),), save_args(self.__update_event, event, callback), block=block) @@ -465,7 +466,7 @@ class ProbeManager(object): currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity) - def install(self, action, block=False): + def install(self, action_name, action, block=False): """ Install an action on the current activity @param action Action to install @@ -473,11 +474,11 @@ class ProbeManager(object): @return None """ if self.currentActivity: - return self._first_proxy(self.currentActivity).install(action, block) + return self._first_proxy(self.currentActivity).install(action_name, action, block) else: raise RuntimeWarning("No activity attached") - def update(self, action, newaction, block=False): + def update(self, action_name, newaction, block=False): """ Update an already installed action's properties and run it again @param action Action to update @@ -486,18 +487,18 @@ class ProbeManager(object): @return None """ if self.currentActivity: - return self._first_proxy(self.currentActivity).update(action, newaction, block) + return self._first_proxy(self.currentActivity).update(action_name, newaction, block) else: raise RuntimeWarning("No activity attached") - def uninstall(self, action, block=False): + def uninstall(self, action_name, block=False): """ Uninstall an installed action @param action Action to uninstall @param block Force a synchroneous dbus call if True """ if self.currentActivity: - return self._first_proxy(self.currentActivity).uninstall(action, block) + return self._first_proxy(self.currentActivity).uninstall(action_name, block) else: raise RuntimeWarning("No activity attached") diff --git a/tutorius/core.py b/tutorius/core.py deleted file mode 100644 index 80e1b4f..0000000 --- a/tutorius/core.py +++ /dev/null @@ -1,640 +0,0 @@ -# Copyright (C) 2009, Tutorius.org -# Copyright (C) 2009, Vincent Vinet <vince.vinet@gmail.com> -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA -""" -Core - -This module contains the core classes for tutorius - -""" - -import logging -import os - -from .TProbe import ProbeManager -from .dbustools import save_args -from . import addon - -logger = logging.getLogger("tutorius") - -class Tutorial (object): - """ - Tutorial Class, used to run through the FSM. - """ - #Properties - probeManager = property(lambda self: self._probeMgr) - activityId = property(lambda self: self._activity_id) - - def __init__(self, name, fsm, filename=None): - """ - Creates an unattached tutorial. - """ - object.__init__(self) - self.name = name - self.activity_init_state_filename = filename - - self.state_machine = fsm - self.state_machine.set_tutorial(self) - - self.state = None - - self.handlers = [] - self._probeMgr = ProbeManager() - self._activity_id = None - #Rest of initialisation happens when attached - - def attach(self, activity_id): - """ - Attach to a running activity - - @param activity_id the id of the activity to attach to - """ - #For now, absolutely detach if a previous one! - if self._activity_id: - self.detach() - self._activity_id = activity_id - self._probeMgr.attach(activity_id) - self._probeMgr.currentActivity = activity_id - self._prepare_activity() - self.state_machine.set_state("INIT") - - def detach(self): - """ - Detach from the current activity - """ - - # Uninstall the whole FSM - self.state_machine.teardown() - - if not self._activity_id is None: - self._probeMgr.detach(self._activity_id) - self._activity_id = None - - def set_state(self, name): - """ - Switch to a new state - """ - logger.debug("==== NEW STATE: %s ====" % name) - - self.state_machine.set_state(name) - - def _prepare_activity(self): - """ - Prepare the activity for the tutorial by loading the saved state and - emitting gtk signals - """ - #Load the saved activity if any - if self.activity_init_state_filename is not None: - #For now the file will be saved in the data folder - #of the activity root directory - filename = os.getenv("SUGAR_ACTIVITY_ROOT") + "/data/" +\ - self.activity_init_state_filename - readfile = addon.create("ReadFile", filename=filename) - if readfile: - self._probeMgr.install(readfile) - #Uninstall now while we have the reference handy - self._probeMgr.uninstall(readfile) - -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 event filters - with associated actions that point to a possible next state. - """ - - def __init__(self, name="", action_list=None, event_filter_list=None, tutorial=None): - """ - Initializes the content of the state, like loading the actions - that are required and building the correct tests. - - @param action_list The list of actions to execute when entering this - state - @param event_filter_list A list of tuples of the form - (event_filter, next_state_name), that explains the outgoing links for - this state - @param tutorial The higher level container of the state - """ - object.__init__(self) - - self.name = name - - self._actions = action_list or [] - - self._transitions= dict(event_filter_list or []) - - self._installedEvents = set() - - self.tutorial = tutorial - - def set_tutorial(self, tutorial): - """ - Associates this state with a tutorial. A tutorial must be set prior - to executing anything in the state. The reason for this is that the - states need to have access to the activity (via the tutorial) in order - to properly register their callbacks on the activities' widgets. - - @param tutorial The tutorial that this state runs under. - """ - if self.tutorial == None : - self.tutorial = tutorial - else: - raise RuntimeWarning(\ - "The state %s was already associated with a tutorial." % self.name) - - def setup(self): - """ - Install the state itself, by first registering the event filters - and then triggering the actions. - """ - for (event, next_state) in self._transitions.items(): - self._installedEvents.add(self.tutorial.probeManager.subscribe(event, save_args(self._event_filter_state_done_cb, next_state ))) - - for action in self._actions: - self.tutorial.probeManager.install(action) - - def teardown(self): - """ - Uninstall all the event filters that were active in this state. - Also undo every action that was installed for this state. This means - removing dialogs that were displayed, removing highlights, etc... - """ - # Remove the handlers for the all of the state's event filters - while len(self._installedEvents) > 0: - self.tutorial.probeManager.unsubscribe(self._installedEvents.pop()) - - # Undo all the actions related to this state - for action in self._actions: - self.tutorial.probeManager.uninstall(action) - - def _event_filter_state_done_cb(self, next_state, event): - """ - Callback for event filters. This function needs to inform the - tutorial that the state is over and tell it what is the next state. - - @param next_state The next state for the transition - @param event The event that occured - """ - # Run the tests here, if need be - - # Warn the higher level that we wish to change state - self.tutorial.set_state(next_state) - - # Model manipulation - # These functions are used to simplify the creation of states - def add_action(self, new_action): - """ - Adds an action to the state - - @param new_action The new action to execute when in this state - @return True if added, False otherwise - """ - self._actions.append(new_action) - return True - - # remove_action - We did not define names for the action, hence they're - # pretty hard to remove on a precise basis - - def get_action_list(self): - """ - @return A list of actions that the state will execute - """ - return self._actions - - def clear_actions(self): - """ - Removes all the action associated with this state. A cleared state will - not do anything when entered or exited. - """ - #FIXME What if the action is currently installed? - self._actions = [] - - def add_event_filter(self, event, next_state): - """ - Adds an event filter that will cause a transition from this state. - - The same event filter may not be added twice. - - @param event The event that will trigger a transition - @param next_state The state to which the transition will lead - @return True if added, False otherwise - """ - if event not in self._transitions.keys(): - self._transitions[event]=next_state - return True - return False - - def get_event_filter_list(self): - """ - @return The list of event filters associated with this state. - """ - return self._transitions.items() - - def clear_event_filters(self): - """ - Removes all the event filters associated with this state. A state that - was just cleared will become a sink and will be the end of the - tutorial. - """ - self._transitions = {} - - def __eq__(self, otherState): - """ - Compares two states and tells whether they contain the same states with the - same actions and event filters. - - @param otherState The other State that we wish to match - @returns True if every action in this state has a matching action in the - other state with the same properties and values AND if every - event filters in this state has a matching filter in the - other state having the same properties and values AND if both - states have the same name. -` """ - if not isinstance(otherState, State): - return False - if self.name != otherState.name: - return False - - # Do they have the same actions? - if len(self._actions) != len(otherState._actions): - return False - - if len(self._transitions) != len(otherState._transitions): - return False - - for act in self._actions: - found = False - # For each action in the other state, try to match it with this one. - for otherAct in otherState._actions: - if act == otherAct: - found = True - break - if found == False: - # If we arrive here, then we could not find an action with the - # same values in the other state. We know they're not identical - return False - - # Do they have the same event filters? - if self._transitions != otherState._transitions: - return False - - # If nothing failed up to now, then every actions and every filters can - # be found in the other state - return True - -class FiniteStateMachine(State): - """ - This is a collection of states, with a start state and an end callback. - It is used to simplify the development of the various tutorials by - encapsulating a collection of states that represent a given learning - process. - - For now, we will consider that there can only be states - inserted in the FSM, and that there are no nested FSM inside. - """ - - def __init__(self, name, tutorial=None, state_dict=None, start_state_name="INIT", action_list=None): - """ - The constructor for a FSM. Pass in the start state and the setup - actions that need to be taken when the FSM itself start (which may be - different from what is done in the first state of the machine). - - @param name A short descriptive name for this FSM - @param tutorial The tutorial that will execute this FSM. If None is - attached on creation, then one must absolutely be attached before - executing the FSM with set_tutorial(). - @param state_dict A dictionary containing the state names as keys and - the state themselves as entries. - @param start_state_name The name of the starting state, if different - from "INIT" - @param action_list The actions to undertake when initializing the FSM - """ - State.__init__(self, name) - - self.name = name - self.tutorial = tutorial - - # Dictionnary of states contained in the FSM - self._states = state_dict or {} - - self.start_state_name = start_state_name - # Set the current state to None - we are not executing anything yet - self.current_state = None - - # Register the actions for the FSM - They will be processed at the - # FSM level, meaning that when the FSM will start, it will first - # execute those actions. When the FSM closes, it will tear down the - # inner actions of the state, then close its own actions - self.actions = action_list or [] - - # Flag to mention that the FSM was initialized - self._fsm_setup_done = False - # Flag that must be raised when the FSM is to be teared down - self._fsm_teardown_done = False - # Flag used to declare that the FSM has reached an end state - self._fsm_has_finished = False - - def set_tutorial(self, tutorial): - """ - This associates the FSM to the given tutorial. It MUST be associated - either in the constructor or with this function prior to executing the - FSM. - - @param tutorial The tutorial that will execute this FSM. - """ - # If there was no tutorial associated - if self.tutorial == None: - # Associate it with this FSM and all the underlying states - self.tutorial = tutorial - for state in self._states.itervalues(): - state.set_tutorial(tutorial) - else: - raise RuntimeWarning(\ - "The FSM %s is already associated with a tutorial."%self.name) - - def setup(self): - """ - This function initializes the FSM the first time it is called. - Then, every time it is called, it initializes the current state. - """ - # Are we associated with a tutorial? - if self.tutorial == None: - raise UnboundLocalError("No tutorial was associated with FSM %s" % self.name) - - # If we never initialized the FSM itself, then we need to run all the - # actions associated with the FSM. - if self._fsm_setup_done == False: - # Remember the initial state - we might want to reset - # or rewind the FSM at a later moment - self.start_state = self._states[self.start_state_name] - self.current_state = self.start_state - # Flag the FSM level setup as done - self._fsm_setup_done = True - # Execute all the FSM level actions - for action in self.actions: - self.tutorial.probeManager.install(action) - - # Then, we need to run the setup of the current state - self.current_state.setup() - - def set_state(self, new_state_name): - """ - This functions changes the current state of the finite state machine. - - @param new_state The identifier of the state we need to go to - """ - # TODO : Since we assume no nested FSMs, we don't set state on the - # inner States / FSMs -## # Pass in the name to the internal state - it might be a FSM and -## # this name will apply to it -## self.current_state.set_state(new_state_name) - - # Make sure the given state is owned by the FSM - if not self._states.has_key(new_state_name): - # If we did not recognize the name, then we do not possess any - # state by that name - we must ignore this state change request as - # it will be done elsewhere in the hierarchy (or it's just bogus). - return - - if self.current_state != None: - if new_state_name == self.current_state.name: - # If we already are in this state, we do not need to change - # anything in the current state - By design, a state may not point - # to itself - return - - new_state = self._states[new_state_name] - - # Undo the actions of the old state - self.teardown() - - # Insert the new state - self.current_state = new_state - - # Call the initial actions in the new state - self.setup() - - def get_current_state_name(self): - """ - Returns the name of the current state. - - @return A string representing the name of the current state - """ - return self.current_state.name - - def teardown(self): - """ - Revert any changes done by setup() - """ - # Teardown the current state - if self.current_state is not None: - self.current_state.teardown() - - # If we just finished the whole FSM, we need to also call the teardown - # on the FSM level actions - if self._fsm_has_finished == True: - # Flag the FSM teardown as not needed anymore - self._fsm_teardown_done = True - # Undo all the FSM level actions here - for action in self.actions: - self.tutorial.probeManager.uninstall(action) - - # TODO : It might be nice to have a start() and stop() method for the - # FSM. - - # Data manipulation section - # These functions are dedicated to the building and editing of a graph. - def add_state(self, new_state): - """ - Inserts a new state in the FSM. - - @param new_state The State object that will now be part of the FSM - @raise KeyError In the case where a state with this name already exists - """ - if self._states.has_key(new_state.name): - raise KeyError("There is already a state by this name in the FSM") - - self._states[new_state.name] = new_state - - # Not such a great name for the state accessor... We already have a - # set_state name, so get_state would conflict with the notion of current - # state - I would recommend having a set_current_state instead. - def get_state_by_name(self, state_name): - """ - Fetches a state from the FSM, based on its name. If there is no - such state, the method will throw a KeyError. - - @param state_name The name of the desired state - @return The State object having the given name - """ - return self._states[state_name] - - def remove_state(self, state_name): - """ - Removes a state from the FSM. Raises a KeyError when the state is - not existent. - - Warning : removing a state will also remove all the event filters that - point to this given name, to preserve the FSM's integrity. If you only - want to edit a state, you would be better off fetching this state with - get_state_by_name(). - - @param state_name A string being the name of the state to remove - @raise KeyError When the state_name does not a represent a real state - stored in the dictionary - """ - - state_to_remove = self._states[state_name] - - # Remove the state from the states' dictionnary - for st in self._states.itervalues(): - # Iterate through the list of event filters and remove those - # that point to the state that will be removed - - #TODO : Move this code inside the State itself - we're breaking - # encap :P - for event in st._transitions: - if st._transitions[event] == state_name: - del st._transitions[event] - - # Remove the state from the dictionary - del self._states[state_name] - - # Exploration methods - used to know more about a given state - 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 - """ - state = self._states[state_name] - - next_states = set() - - for event, state in state._transitions.items(): - next_states.add(state) - - return tuple(next_states) - - 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. - """ - # This might seem a bit funny, but we don't verify if the given - # state is present or not in the dictionary. - # This is due to the fact that when building a graph, we might have a - # prototypal state that has not been inserted yet. We could not know - # which states are pointing to it until we insert it in the graph. - - states = [] - # Walk through the list of states - for st in self._states.itervalues(): - for event, state in st._transitions.items(): - if state == state_name: - states.append(state) - continue - - return tuple(states) - - # Convenience methods to see the content of a FSM - def __str__(self): - out_string = "" - for st in self._states.itervalues(): - out_string += st.name + ", " - return out_string - - def __eq__(self, otherFSM): - """ - Compares the elements of two FSM to ensure and returns true if they have the - same set of states, containing the same actions and the same event filters. - - @returns True if the two FSMs have the same content, False otherwise - """ - if not isinstance(otherFSM, FiniteStateMachine): - return False - - # Make sure they share the same name - if not (self.name == otherFSM.name) or \ - not (self.start_state_name == otherFSM.start_state_name): - return False - - # Ensure they have the same number of FSM-level actions - if len(self._actions) != len(otherFSM._actions): - return False - - # Test that we have all the same FSM level actions - for act in self._actions: - found = False - # For every action in the other FSM, try to match it with the - # current one. - for otherAct in otherFSM._actions: - if act == otherAct: - found = True - break - if found == False: - return False - - # Make sure we have the same number of states in both FSMs - if len(self._states) != len(otherFSM._states): - return False - - # For each state, try to find a corresponding state in the other FSM - for state_name in self._states.keys(): - state = self._states[state_name] - other_state = None - try: - # Attempt to use this key in the other FSM. If it's not present - # the dictionary will throw an exception and we'll know we have - # at least one different state in the other FSM - other_state = otherFSM._states[state_name] - except: - return False - # If two states with the same name exist, then we want to make sure - # they are also identical - if not state == other_state: - return False - - # If we made it here, then all the states in this FSM could be matched to an - # identical state in the other FSM. - return True - if len(self._states) != len(otherFSM._states): - return False - - # For each state, try to find a corresponding state in the other FSM - for state_name in self._states.keys(): - state = self._states[state_name] - other_state = None - try: - # Attempt to use this key in the other FSM. If it's not present - # the dictionary will throw an exception and we'll know we have - # at least one different state in the other FSM - other_state = otherFSM._states[state_name] - except: - return False - # If two states with the same name exist, then we want to make sure - # they are also identical - if not state == other_state: - return False - - # If we made it here, then all the states in this FSM could be matched to an - # identical state in the other FSM. - return True diff --git a/tutorius/engine.py b/tutorius/engine.py index a3952ad..ec281b3 100644 --- a/tutorius/engine.py +++ b/tutorius/engine.py @@ -26,8 +26,7 @@ class TutorialRunner(object): self._state = None self._sEvents = set() #Subscribed Events - #Cached objects - self._actions = {} + self._installed_actions = {} #Temp FIX until event/actions have an activity id self._activity_id = None @@ -56,15 +55,18 @@ class TutorialRunner(object): return #Clear the current actions - for action in self._actions.values(): - self._pM.uninstall(action) - self._actions = {} + for action_address in self._installed_actions.values(): + self._pM.uninstall(action_address) + self._installed_actions = {} #Clear the EventFilters for event in self._sEvents: self._pM.unsubscribe(event) self._sEvents.clear() + def __save_address(self, action_name, action_address): + self._installed_actions[action_name] = action_address + def _setupState(self): if self._state is None: raise RuntimeError("Attempting to setupState without a state") @@ -72,19 +74,25 @@ class TutorialRunner(object): # Handle the automatic event state_name = self._state - self._actions = self._tutorial.get_action_dict(self._state) + actions = self._tutorial.get_action_dict(self._state) transitions = self._tutorial.get_transition_dict(self._state) + # Verify if we have an automatic transition in the state - if so, we + # will skip installing the actions and events and go straight to the + # next state for (event, next_state) in transitions.values(): if isinstance(event, AutomaticTransitionEvent): state_name = next_state - break + return state_name + + # Install all the actions first + for (action_name, action) in actions.items(): + self._pM.install(action, save_args(self.__save_address, action_name), block=True) + # Install the event filters + for (event, next_state) in transitions.values(): self._sEvents.add(self._pM.subscribe(event, save_args(self._handleEvent, next_state))) - for action in self._actions.values(): - self._pM.install(action) - return state_name def enterState(self, state_name): @@ -113,9 +121,6 @@ class TutorialRunner(object): # transition in the state definition self.enterState(self._setupState()) - - - class Engine: """ Driver for the execution of tutorials diff --git a/tutorius/translator.py b/tutorius/translator.py index 9cd4f98..335a461 100644 --- a/tutorius/translator.py +++ b/tutorius/translator.py @@ -163,7 +163,7 @@ class ResourceTranslator(object): return self._probe_manager.get_registered_probes_list(process_name) ## Decorated functions ## - def install(self, action, block=False): + def install(self, action, callback, block=False): # Make a new copy of the action that we want to install, # because translate() changes the action and we # don't want to modify the caller's action representation @@ -172,9 +172,9 @@ class ResourceTranslator(object): self.translate(new_action) # Send the new action to the probe manager - return self._probe_manager.install(new_action, block) + return self._probe_manager.install(new_action, callback, block) - def update(self, action, newaction, block=False): + def update(self, action_address, newaction, block=False): # TODO : Repair this as it currently doesn't work. # Actions are being copied, then translated in install(), so they # won't be addressable via the same object that is in the Tutorial @@ -182,11 +182,8 @@ class ResourceTranslator(object): translated_new_action = copy_module.deepcopy(newaction) self.translate(translated_new_action) - return self._probe_manager.update(action, translated_new_action, block) + return self._probe_manager.update(action_address, translated_new_action, block) - def uninstall(self, action, block=False): - new_action = copy_module.deepcopy(action) - self.translate(new_action) - - return self._probe_manager.uninstall(new_action, block) + def uninstall(self, action_address, block=False): + return self._probe_manager.uninstall(action_address, block) diff --git a/tutorius/tutorial.py b/tutorius/tutorial.py index b45363f..793d6f2 100644 --- a/tutorius/tutorial.py +++ b/tutorius/tutorial.py @@ -88,7 +88,7 @@ class Tutorial(object): self._state_name_nb = 0 - def add_state(self, action_list=(), transition_list=()): + def add_state(self, action_dict={}, transition_list=()): """ Add a new state to the state machine. The state is initialized with the action list and transition list @@ -98,19 +98,19 @@ class Tutorial(object): The transitions are added using add_transition. - @param action_list The list of valid actions for this state + @param action_dict The dictionary of valid action_name: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: + for (action_name, action) in action_dict.items(): self._validate_action(action) for transition in transition_list: self._validate_transition(transition) - state = State(name, action_list, transition_list) + state = State(name, action_dict, transition_list) self._state_dict[name] = state diff --git a/tutorius/vault.py b/tutorius/vault.py index 73f98d0..af00539 100644 --- a/tutorius/vault.py +++ b/tutorius/vault.py @@ -807,10 +807,10 @@ class XMLSerializer(Serializer): stateName = state.getAttribute("Name") # Using item 0 in the list because there is always only one # Actions and EventFilterList element per State node. - actions_list = cls._load_xml_actions(state.getElementsByTagName(ELEM_ACTIONS)[0]) + actions_dict = cls._load_xml_actions(state.getElementsByTagName(ELEM_ACTIONS)[0]) transitions_list = cls._load_xml_transitions(state.getElementsByTagName(ELEM_TRANS)[0]) - state_dict[stateName] = State(stateName, actions_list, transitions_list) + state_dict[stateName] = State(stateName, actions_dict, transitions_list) return state_dict |