From 80c7d20f83d15890a31b5e0a584dfcb24fb3f2a6 Mon Sep 17 00:00:00 2001 From: JCTutorius Date: Thu, 05 Nov 2009 18:42:53 +0000 Subject: Merge branch 'master' of gitorious@git.sugarlabs.org:tutorius/mainline Conflicts: tests/vaulttests.py tutorius/vault.py --- diff --git a/tests/vaulttests.py b/tests/vaulttests.py index d6787ef..d085faf 100644 --- a/tests/vaulttests.py +++ b/tests/vaulttests.py @@ -29,9 +29,10 @@ import unittest import os import shutil import zipfile +import cStringIO from sugar.tutorius import addon -from sugar.tutorius.core import State, FiniteStateMachine, Tutorial +from sugar.tutorius.tutorial import Tutorial from sugar.tutorius.actions import * from sugar.tutorius.filters import * from sugar.tutorius.vault import Vault, XMLSerializer, Serializer, TutorialBundler @@ -100,18 +101,14 @@ class VaultInterfaceTest(unittest.TestCase): ini_file2.close() # Create a dummy fsm - self.fsm = FiniteStateMachine("testingMachine") + self.fsm = Tutorial("TestTutorial1") # Add a few states act1 = addon.create('BubbleMessage', message="Hi", position=[300, 450]) ev1 = addon.create('GtkWidgetEventFilter', "0.12.31.2.2", "clicked") act2 = addon.create('BubbleMessage', message="Second message", position=[250, 150], tail_pos=[1,2]) - st1 = State("INIT") - st1.add_action(act1) - st1.add_event_filter(ev1, 'Second') - st2 = State("Second") - st2.add_action(act2) - self.fsm.add_state(st1) - self.fsm.add_state(st2) + self.fsm.add_action("INIT", act1) + st2 = self.fsm.add_state((act2,)) + self.fsm.add_transition("INIT",(ev1, st2)) self.tuto_guid = uuid1() # Create a dummy metadata dictionnary @@ -146,7 +143,8 @@ class VaultInterfaceTest(unittest.TestCase): # Creat a dummy tutorial .xml file serializer = XMLSerializer() - serializer.save_fsm(self.fsm, 'tutorial.xml', test_path) + with file(os.path.join(test_path, 'tutorial.xml'), 'w') as fsmfile: + serializer.save_tutorial(self.fsm, fsmfile) # Create a dummy tutorial metadata file self.create_test_metadata_file(os.path.join(test_path, 'meta.ini'), self.tuto_guid) @@ -238,16 +236,8 @@ class VaultInterfaceTest(unittest.TestCase): reloaded_tuto = vault.loadTutorial(self.tuto_guid) # Compare the two FSMs - reloaded_fsm = reloaded_tuto.state_machine - - assert reloaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \ - 'FSM underlying dictionary differ from original to reformed one' - assert reloaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \ + assert reloaded_tuto.get_state_dict().keys() == self.fsm.get_state_dict().keys(), \ 'FSM underlying dictionary differ from original to reformed one' - assert reloaded_fsm._states.get("INIT").get_action_list()[0].message == \ - self.fsm._states.get("INIT").get_action_list()[0].message, \ - 'FSM underlying State underlying Action differ from original to reformed one' - assert len(reloaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself" def test_saveTutorial(self): """ @@ -256,35 +246,27 @@ class VaultInterfaceTest(unittest.TestCase): # Save the tutorial in the vault vault = Vault() - tutorial = Tutorial('test', self.fsm) + tutorial = self.fsm vault.saveTutorial(tutorial, self.test_metadata_dict) # Get the tutorial back reloaded_tuto = vault.loadTutorial(self.save_test_guid) # Compare the two FSMs - reloaded_fsm = reloaded_tuto.state_machine - - assert reloaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \ + assert reloaded_tuto.get_state_dict().keys() == self.fsm.get_state_dict().keys(), \ 'FSM underlying dictionary differ from original to reformed one' - assert reloaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \ - 'FSM underlying dictionary differ from original to reformed one' - assert reloaded_fsm._states.get("INIT").get_action_list()[0].message == \ - self.fsm._states.get("INIT").get_action_list()[0].message, \ - 'FSM underlying State underlying Action differ from original to reformed one' - assert len(reloaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself" # TODO : Compare the initial and reloaded metadata when vault.Query() will accept specifc argument # (so we can specifiy that we want only the metadata for this particular tutorial - def test_add_delete_get_path_ressource(self): + def test_add_delete_get_path_resource(self): """ - This test verify that the vault interface function add_ressource succesfully add ressource in the vault - and return the new ressource id. It also test the deletion of the ressource. + This test verify that the vault interface function add_resource succesfully add resource in the vault + and return the new resource id. It also test the deletion of the resource. """ # Path of an image file in the test folder - image_path = os.path.join(os.getcwd(), 'tests', 'ressources', 'icon.svg') + image_path = os.path.join(os.getcwd(), 'tests', 'resources', 'icon.svg') assert os.path.isfile(image_path), 'cannot find the test image file' # Create and save a tutorial @@ -294,30 +276,28 @@ class VaultInterfaceTest(unittest.TestCase): bundler = TutorialBundler(self.save_test_guid) tuto_path = bundler.get_tutorial_path(self.save_test_guid) - # add the ressource to the tutorial - ressource_id = Vault.add_ressource(self.save_test_guid, image_path) + # add the resource to the tutorial + resource_id = Vault.add_resource(self.save_test_guid, image_path) # Check that the image file is now in the vault - assert os.path.isfile(os.path.join(tuto_path, 'ressources', ressource_id)), 'image file not found in vault' + assert os.path.isfile(os.path.join(tuto_path, 'resources', resource_id)), 'image file not found in vault' - # Check if get_ressource_path Vault interface function is working - vault_path = Vault.get_ressource_path(self.save_test_guid, ressource_id) + # Check if get_resource_path Vault interface function is working + vault_path = Vault.get_resource_path(self.save_test_guid, resource_id) assert os.path.isfile(vault_path), 'path returned is not a file' basename, extension = os.path.splitext(vault_path) assert extension == '.svg', 'wrong file path have been returned' - # Delete the ressource - Vault.delete_ressource(self.save_test_guid, ressource_id) + # Delete the resource + Vault.delete_resource(self.save_test_guid, resource_id) - # Check that the ressource is not in the vault anymore - assert os.path.isfile(os.path.join(tuto_path, 'ressources', ressource_id)) == False, 'image file found in vault when it should have been deleted.' + # Check that the resource is not in the vault anymore + assert os.path.isfile(os.path.join(tuto_path, 'resources', resource_id)) == False, 'image file found in vault when it should have been deleted.' - - def tearDown(self): folder = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'data'); for file in os.listdir(folder): @@ -337,8 +317,8 @@ class SerializerInterfaceTest(unittest.TestCase): ser = Serializer() try: - ser.save_fsm(None) - assert False, "save_fsm() should throw an unimplemented error" + ser.save_tutorial(None) + assert False, "save_tutorial() should throw an unimplemented error" except: pass @@ -346,8 +326,8 @@ class SerializerInterfaceTest(unittest.TestCase): ser = Serializer() try: - ser.load_fsm(str(uuid.uuid1())) - assert False, "load_fsm() should throw an unimplemented error" + ser.load_tutorial(str(uuid.uuid1())) + assert False, "load_tutorial() should throw an unimplemented error" except: pass @@ -357,103 +337,61 @@ class XMLSerializerTest(unittest.TestCase): """ def setUp(self): - - os.environ["SUGAR_BUNDLE_PATH"] = os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path') - path = os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path') - if os.path.isdir(path) != True: - os.makedirs(path) - # Create the sample FSM - self.fsm = FiniteStateMachine("testingMachine") + self.fsm = Tutorial("TestTutorial1") # Add a few states act1 = addon.create('BubbleMessage', message="Hi", position=[300, 450]) ev1 = addon.create('GtkWidgetEventFilter', "0.12.31.2.2", "clicked") act2 = addon.create('BubbleMessage', message="Second message", position=[250, 150], tail_pos=[1,2]) - st1 = State("INIT") - st1.add_action(act1) - st1.add_event_filter(ev1, 'Second') - - st2 = State("Second") - - st2.add_action(act2) - - self.fsm.add_state(st1) - self.fsm.add_state(st2) + self.fsm.add_action("INIT",act1) + st2 = self.fsm.add_state((act2,)) + self.fsm.add_transition("INIT",(ev1, st2)) self.uuid = uuid1() - # Flag to set to True if the output can be deleted after execution of - # the test - self.remove = True - def tearDown(self): """ - Removes the created files, if need be. + Nothing to do anymore. """ - if self.remove == True: - shutil.rmtree(os.path.join(sugar.tutorius.vault._get_store_root(), 'test_bundle_path')) - - folder = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'data'); - for file in os.listdir(folder): - file_path = os.path.join(folder, file) - shutil.rmtree(file_path) + pass - def create_test_metadata(self, ini_file_path, guid): - """ - Create a basinc .ini file for testing purpose. - """ - ini_file = open(ini_file_path, 'wt') - ini_file.write("[GENERAL_METADATA]\n") - ini_file.write('guid=' + str(guid) + '\n') - ini_file.write('name=TestTutorial1\n') - ini_file.write('version=1\n') - ini_file.write('description=This is a test tutorial 1\n') - ini_file.write('rating=3.5\n') - ini_file.write('category=Test\n') - ini_file.write('publish_state=false\n') - ini_file.write('[RELATED_ACTIVITIES]\n') - ini_file.write('org.laptop.TutoriusActivity = 1\n') - ini_file.write('org.laptop.Writus = 1\n') - ini_file.close() - - def test_save(self): - """ - Writes an FSM to disk, then compares the file to the expected results. - "Remove" boolean argument specify if the test data must be removed or not - """ - xml_ser = XMLSerializer() - os.makedirs(os.path.join(sugar.tutorius.vault._get_store_root(), str(self.uuid))) - xml_ser.save_fsm(self.fsm, sugar.tutorius.vault.TUTORIAL_FILENAME, os.path.join(sugar.tutorius.vault._get_store_root(), str(self.uuid))) - self.create_test_metadata(os.path.join(sugar.tutorius.vault._get_store_root(), str(self.uuid), 'meta.ini'), self.uuid) - + def create_test_metadata(self, file_obj, guid): + file_obj.write("[GENERAL_METADATA]\n") + file_obj.write('guid=' + str(guid) + '\n') + file_obj.write('name=TestTutorial1\n') + file_obj.write('version=1\n') + file_obj.write('description=This is a test tutorial 1\n') + file_obj.write('rating=3.5\n') + file_obj.write('category=Test\n') + file_obj.write('publish_state=false\n') + file_obj.write('[RELATED_ACTIVITIES]\n') + file_obj.write('org.laptop.TutoriusActivity = 1\n') + file_obj.write('org.laptop.Writus = 1\n') def test_save_and_load(self): """ + Writes an FSM to disk, then compares the file to the expected results. Load up the written FSM and compare it with the object representation. """ - self.test_save() xml_ser = XMLSerializer() - - loaded_fsm = xml_ser.load_fsm(str(self.uuid)) + tuto_file = cStringIO.StringIO() + xml_ser.save_tutorial(self.fsm, tuto_file) + + xml_ser = XMLSerializer() + load_tuto_file = cStringIO.StringIO(tuto_file.getvalue()) + loaded_fsm = xml_ser.load_tutorial(load_tuto_file) # Compare the two FSMs - assert loaded_fsm._states.get("INIT").name == self.fsm._states.get("INIT").name, \ - 'FSM underlying dictionary differ from original to reformed one' - assert loaded_fsm._states.get("Second").name == self.fsm._states.get("Second").name, \ - 'FSM underlying dictionary differ from original to reformed one' - assert loaded_fsm._states.get("INIT").get_action_list()[0].message == \ - self.fsm._states.get("INIT").get_action_list()[0].message, \ - 'FSM underlying State underlying Action differ from original to reformed one' - assert len(loaded_fsm.get_action_list()) == 0, "FSM should not have any actions on itself" + assert loaded_fsm == self.fsm, "Loaded FSM differs from original one" def test_all_actions(self): """ Inserts all the known action types in a FSM, then attempt to load it. """ - st = State("INIT") - + fsm = Tutorial("TestActions") + tuto_file = cStringIO.StringIO() act1 = addon.create('BubbleMessage', "Hi!", position=[10,120], tail_pos=[-12,30]) act2 = addon.create('DialogMessage', "Hello again.", position=[120,10]) act3 = addon.create('WidgetIdentifyAction') @@ -465,26 +403,24 @@ class XMLSerializerTest(unittest.TestCase): actions = [act1, act2, act3, act4, act5, act6, act7, act8] for action in actions: - st.add_action(action) + fsm.add_action("INIT", action) - self.fsm.remove_state("Second") - self.fsm.remove_state("INIT") - self.fsm.add_state(st) - xml_ser = XMLSerializer() + xml_ser.save_tutorial(fsm, tuto_file) + load_tuto_file = cStringIO.StringIO(tuto_file.getvalue()) - self.test_save() - - reloaded_fsm = xml_ser.load_fsm(str(self.uuid)) - - # TODO : Cannot do object equivalence, must check equality of all underlying object - # assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading." + reloaded_fsm = xml_ser.load_tutorial(load_tuto_file) + # Compare the two FSMs + assert reloaded_fsm == fsm, "Loaded FSM differs from original one" + assert fsm.get_action_dict() == reloaded_fsm.get_action_dict(), \ + "Actions should be the same" def test_all_filters(self): """ Inserts all the known action filters in a FSM, then attempt to load it. """ - st = State("INIT") + fsm = Tutorial("TestFilters") + tuto_file = cStringIO.StringIO() ev1 = addon.create('TimerEvent', 1000) ev2 = addon.create('GtkWidgetEventFilter', object_id="0.0.1.1.0.0.1", event_name="clicked") @@ -492,20 +428,18 @@ class XMLSerializerTest(unittest.TestCase): ev4 = addon.create('GtkWidgetTypeFilter', "0.0.1.1.1.2.3", strokes="acbd") filters = [ev1, ev2, ev3, ev4] - for filter in filters: - st.add_event_filter(filter, 'Second') + for efilter in filters: + fsm.add_transition("INIT", (efilter, 'END')) - self.fsm.remove_state("INIT") - self.fsm.add_state(st) - xml_ser = XMLSerializer() - - self.test_save() - - reloaded_fsm = xml_ser.load_fsm(str(self.uuid)) - - # TODO : Cannot do object equivalence, must check equality of all underlying object - # assert self.fsm == reloaded_fsm, "Expected equivalence before saving vs after loading." + xml_ser.save_tutorial(fsm, tuto_file) + load_tuto_file = cStringIO.StringIO(tuto_file.getvalue()) + + reloaded_fsm = xml_ser.load_tutorial(load_tuto_file) + # Compare the two FSMs + assert reloaded_fsm == fsm, "Loaded FSM differs from original one" + assert fsm.get_transition_dict() == reloaded_fsm.get_transition_dict(), \ + "Transitions should be the same" class TutorialBundlerTests(unittest.TestCase): @@ -514,7 +448,7 @@ class TutorialBundlerTests(unittest.TestCase): This module contains all the tests for the storage mecanisms for tutorials This mean testing saving and loading tutorial, .ini file management and - adding ressources to tutorial + adding resources to tutorial """ def setUp(self): @@ -555,4 +489,4 @@ class TutorialBundlerTests(unittest.TestCase): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tutorius/tutorial.py b/tutorius/tutorial.py index 9831a7b..b45363f 100644 --- a/tutorius/tutorial.py +++ b/tutorius/tutorial.py @@ -67,7 +67,8 @@ class Tutorial(object): self.add_transition(Tutorial.INIT, \ (AutomaticTransitionEvent(), Tutorial.END)) else: - raise NotImplementedError("Tutorial: Initilization from a dictionary is not supported yet") + self._state_dict = state_dict + # Minimally check for the presence of an INIT and an END @@ -528,15 +529,20 @@ class Tutorial(object): def _generate_unique_state_name(self): name = "State" + str(self._state_name_nb) - self._state_name_nb += 1 + while name in self._state_dict: + self._state_name_nb += 1 + name = "State" + str(self._state_name_nb) return name + # Python Magic Methods def __str__(self): """ Return a string representation of the tutorial """ return str(self._state_dict) + def __eq__(self, other): + return isinstance(other, type(self)) and self.get_state_dict() == other.get_state_dict() class State(object): """ @@ -548,16 +554,20 @@ class State(object): inputs, the validation should be done by the containing class. """ - def __init__(self, name, action_list=(), transition_list=()): + def __init__(self, name, actions={}, transitions={}): """ 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 + @param actions list or dict of actions to perform when entering the state - @param transition_list A list of tuples of the form + @param transitions list or dict of tuples of the form (event, next_state_name), that explains the outgoing links for this state + + For actions and transitions, dictionaries allow specifying the name. + If lists are given, their contents will be added with add_action or + add_transition """ object.__init__(self) @@ -567,13 +577,19 @@ class State(object): self.action_name_nb = 0 self.transition_name_nb = 0 - self._actions = {} - for action in action_list: - self.add_action(action) - - self._transitions = {} - for transition in transition_list: - self.add_transition(transition) + if type(actions) is dict: + self._actions = dict(actions) + else: + self._actions = {} + for action in actions: + self.add_action(action) + + if type(transitions) is dict: + self._transitions = dict(transitions) + else: + self._transitions = {} + for transition in transitions: + self.add_transition(transition) # Action manipulations @@ -741,7 +757,9 @@ class State(object): # to make it easier to debug and know what we are # manipulating name = self.name + _NAME_SEPARATOR + "action" + str(self.action_name_nb) - self.action_name_nb += 1 + while name in self._actions: + self.action_name_nb += 1 + name = self.name + _NAME_SEPARATOR + "action" + str(self.action_name_nb) return name def _generate_unique_transition_name(self, transition): @@ -757,7 +775,9 @@ class State(object): # generate a name to make it easier to debug and know # what we are manipulating name = self.name + _NAME_SEPARATOR + "transition" + str(self.transition_name_nb) - self.transition_name_nb += 1 + while name in self._transitions: + self.transition_name_nb += 1 + name = self.name + _NAME_SEPARATOR + "transition" + str(self.transition_name_nb) return name def __eq__(self, otherState): @@ -775,12 +795,15 @@ class State(object): @param otherState The state that will be compared to this one @return True if the states are the same, False otherwise ` """ - raise NotImplementedError + return isinstance(otherState, type(self)) and \ + self.get_action_dict() == otherState.get_action_dict() and \ + self.get_transition_dict() == otherState.get_transition_dict() #TODO: Define the automatic transition in the same way as # other events class AutomaticTransitionEvent(TPropContainer): - pass + def __repr__(self): + return str(self.__class__.__name__) ################## Error Handling and Exceptions ############################## diff --git a/tutorius/vault.py b/tutorius/vault.py index 9cda2e9..728cf64 100644 --- a/tutorius/vault.py +++ b/tutorius/vault.py @@ -28,10 +28,10 @@ import uuid import xml.dom.minidom from xml.dom import NotFoundErr import zipfile +from ConfigParser import SafeConfigParser from . import addon -from .core import Tutorial, State, FiniteStateMachine -from ConfigParser import SafeConfigParser +from .tutorial import Tutorial, State, AutomaticTransitionEvent logger = logging.getLogger("tutorius") @@ -58,11 +58,23 @@ INI_XML_FSM_PROPERTY = "fsm_filename" INI_VERSION_PROPERTY = 'version' INI_FILENAME = "meta.ini" TUTORIAL_FILENAME = "tutorial.xml" +RESOURCES_FOLDER = 'resources' + +###################################################################### +# XML Tag names and attributes +###################################################################### +ELEM_FSM = "FSM" +ELEM_STATES = "States" +ELEM_STATE = "State" +ELEM_ACTIONS = "Actions" +ELEM_TRANS = "Transitions" +ELEM_AUTOTRANS = "AutomaticTransition" NODE_COMPONENT = "Component" NODE_SUBCOMPONENT = "property" NODE_SUBCOMPONENTLIST = "listproperty" -NEXT_STATE_ATTR = "next_state" -RESSOURCES_FOLDER = 'ressources' +NAME_ATTR = "__name__" +NEXT_STATE_ATTR = "__next_state__" +START_STATE_ATTR = "__start_state__" class Vault(object): @@ -74,7 +86,7 @@ class Vault(object): given activity. @param activity_name the name of the activity associated with this tutorial. None means ALL activities - @param activity_vers the version number of the activity to find tutorail for. 0 means find for ANY version. Ifactivity_ame is None, version number is not used + @param activity_vers the version number of the activity to find tutorial for. 0 means find for ANY version. If activity_name is None, version number is not used @returns a map of tutorial {names : GUID}. """ # check both under the activity data and user installed folders @@ -238,12 +250,14 @@ class Vault(object): # Return tutorial list return tutorial_list + @staticmethod def loadTutorial(Guid): """ Creates an executable version of a tutorial from its saved representation. - @returns an executable representation of a tutorial + @param Guid Unique identifier of the tutorial + @returns Tutorial object """ bundle = TutorialBundler(Guid) @@ -254,15 +268,20 @@ class Vault(object): serializer = XMLSerializer() name = config.get(INI_METADATA_SECTION, INI_NAME_PROPERTY) - fsm = serializer.load_fsm(Guid, bundle_path) - tuto = Tutorial(name, fsm) - return tuto + # Open the XML file + tutorial_file = os.path.join(bundle_path, TUTORIAL_FILENAME) + with open(tutorial_file, 'r') as tfile: + tutorial = serializer.load_tutorial(tfile) + + return tutorial @staticmethod def saveTutorial(tutorial, metadata_dict): """ Creates a persistent version of a tutorial in the Vault. + @param tutorial Tutorial + @param metadata_dict dictionary of metadata for the Tutorial @returns true if the tutorial was saved correctly """ @@ -276,7 +295,9 @@ class Vault(object): # Serialize the tutorial and write it to disk xml_ser = XMLSerializer() os.makedirs(tutorial_path) - xml_ser.save_fsm(tutorial.state_machine, TUTORIAL_FILENAME, tutorial_path) + + with open(os.path.join(tutorial_path, TUTORIAL_FILENAME), 'w') as fsmfile: + xml_ser.save_tutorial(tutorial, fsmfile) # Create the metadata file ini_file_path = os.path.join(tutorial_path, "meta.ini") @@ -321,32 +342,34 @@ class Vault(object): else: return False - + @staticmethod - def add_ressource(tutorial_guid, file_path): + def add_resource(tutorial_guid, file_path): """ - Returns a unique name for this resource composed from the original name of the file - and a suffix to make it unique ( ex: name_1.jpg ) and add it to the resources for the tutorial. + Add given resource file in the vault and returns a unique name for this resource + composed from the original name of the file and a suffix to make it unique + ( ex: name_1.jpg ). @param tutorial_guid The guid of the tutorial - @param file_path the file path of the ressource to add - @returns the ressource_id of the ressource + @param file_path the file path of the resource to add + @returns the resource_id of the resource """ # Get the tutorial path bundler = TutorialBundler(tutorial_guid) tutorial_path = bundler.get_tutorial_path(tutorial_guid) # Get the file name - fname_splitted = file_path.rsplit('/') - file_name = fname_splitted[fname_splitted.__len__() - 1] + file_name = os.path.basename(file_path) + #fname_splitted = file_path.rsplit('/') + #file_name = fname_splitted[fname_splitted.__len__() - 1] base_name, extension = os.path.splitext(file_name) # Append unique name to file name file_name_appended = base_name + '_' + str(uuid.uuid1()) + extension - # Check if the ressource file already exists - new_file_path = os.path.join(tutorial_path, RESSOURCES_FOLDER, file_name_appended) + # Check if the resource file already exists + new_file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, file_name_appended) if os.path.isfile(new_file_path) == False: - # Copy the ressource file in the vault - if os.path.isdir(os.path.join(tutorial_path, RESSOURCES_FOLDER)) == False: - os.makedirs(os.path.join(tutorial_path, RESSOURCES_FOLDER)) + # Copy the resource file in the vault + if os.path.isdir(os.path.join(tutorial_path, RESOURCES_FOLDER)) == False: + os.makedirs(os.path.join(tutorial_path, RESOURCES_FOLDER)) assert os.path.isfile(file_path) shutil.copyfile(file_path, new_file_path) @@ -354,36 +377,36 @@ class Vault(object): @staticmethod - def delete_ressource(tutorial_guid, ressource_id): + def delete_resource(tutorial_guid, resource_id): """ Delete the resource from the resources of the tutorial. @param tutorial_guid the guid of the tutorial - @param ressource_id the ressource id of the ressource to delete + @param resource_id the resource id of the resource to delete """ # Get the tutorial path bundler = TutorialBundler(tutorial_guid) tutorial_path = bundler.get_tutorial_path(tutorial_guid) - # Check if the ressource file exists - file_path = os.path.join(tutorial_path, RESSOURCES_FOLDER, ressource_id) + # Check if the resource file exists + file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, resource_id) if os.path.isfile(file_path): - # Delete the ressource + # Delete the resource os.remove(file_path) else: print('File not found, no delete took place') @staticmethod - def get_ressource_path(tutorial_guid, ressource_id): + def get_resource_path(tutorial_guid, resource_id): """ Returns the absolute file path to the resourceID @param tutorial_guid the guid of the tutorial - @param ressource_id the ressource id of the ressource to find the path for - @returns the absolute path of the ressource file + @param resource_id the resource id of the resource to find the path for + @returns the absolute path of the resource file """ # Get the tutorial path bundler = TutorialBundler(tutorial_guid) tutorial_path = bundler.get_tutorial_path(tutorial_guid) - # Check if the ressource file exists - file_path = os.path.join(tutorial_path, RESSOURCES_FOLDER, ressource_id) + # Check if the resource file exists + file_path = os.path.join(tutorial_path, RESOURCES_FOLDER, resource_id) if os.path.isfile(file_path): return file_path else: @@ -396,7 +419,7 @@ class Serializer(object): used in the tutorials to/from disk. Must be inherited. """ - def save_fsm(self,fsm): + def save_tutorial(self,fsm): """ Save fsm to disk. If a GUID parameter is provided, the existing GUID is located in the .ini files in the store root and bundle root and @@ -406,7 +429,7 @@ class Serializer(object): """ raise NotImplementedError() - def load_fsm(self): + def load_tutorial(self): """ Load fsm from disk. """ @@ -417,21 +440,26 @@ class XMLSerializer(Serializer): Class that provide serializing and deserializing of the FSM used in the tutorials to/from a .xml file. Inherit from Serializer """ - - def _create_state_dict_node(self, state_dict, doc): + + @classmethod + def _create_state_dict_node(cls, state_dict, doc): """ Create and return a xml Node from a State dictionnary. + @param state_dict dictionary of State objects + @param doc The XML document root (used to create nodes only + @return xml Element containing the states """ - statesList = doc.createElement("States") + statesList = doc.createElement(ELEM_STATES) for state_name, state in state_dict.items(): - stateNode = doc.createElement("State") + stateNode = doc.createElement(ELEM_STATE) statesList.appendChild(stateNode) stateNode.setAttribute("Name", state_name) - actionsList = stateNode.appendChild(self._create_action_list_node(state.get_action_list(), doc)) - eventfiltersList = stateNode.appendChild(self._create_event_filters_node(state.get_event_filter_list(), doc)) + actionsList = stateNode.appendChild(cls._create_action_list_node(state.get_action_dict(), doc)) + transitionsList = stateNode.appendChild(cls._create_transitions_node(state.get_transition_dict(), doc)) return statesList - - def _create_addon_component_node(self, parent_attr_name, comp, doc): + + @classmethod + def _create_addon_component_node(cls, parent_attr_name, comp, doc): """ Takes a component that is embedded in another component (e.g. the content of a OnceWrapper) and encapsulate it in a node with the property name. @@ -458,13 +486,14 @@ class XMLSerializer(Serializer): subCompNode = doc.createElement(NODE_SUBCOMPONENT) subCompNode.setAttribute("name", parent_attr_name) - subNode = self._create_component_node(comp, doc) + subNode = cls._create_component_node(comp, doc) subCompNode.appendChild(subNode) return subCompNode - def _create_addonlist_component_node(self, parent_attr_name, comp_list, doc): + @classmethod + def _create_addonlist_component_node(cls, parent_attr_name, comp_list, doc): """ Takes a list of components that are embedded in another component (ex. the content of a ChainAction) and encapsulate them in a node with the property @@ -491,12 +520,13 @@ class XMLSerializer(Serializer): subCompListNode.setAttribute("name", parent_attr_name) for comp in comp_list: - compNode = self._create_component_node(comp, doc) + compNode = cls._create_component_node(comp, doc) subCompListNode.appendChild(compNode) return subCompListNode - def _create_component_node(self, comp, doc): + @classmethod + def _create_component_node(cls, comp, doc): """ Takes a single component (action or eventfilter) and transforms it into a xml node. @@ -515,68 +545,86 @@ class XMLSerializer(Serializer): for propname in comp.get_properties(): propval = getattr(comp, propname) if getattr(type(comp), propname).type == "addonlist": - compNode.appendChild(self._create_addonlist_component_node(propname, propval, doc)) + compNode.appendChild(cls._create_addonlist_component_node(propname, propval, doc)) elif getattr(type(comp), propname).type == "addon": #import rpdb2; rpdb2.start_embedded_debugger('pass') - compNode.appendChild(self._create_addon_component_node(propname, propval, doc)) + compNode.appendChild(cls._create_addon_component_node(propname, propval, doc)) else: # repr instead of str, as we want to be able to eval() it into a # valid object. compNode.setAttribute(propname, repr(propval)) return compNode - - def _create_action_list_node(self, action_list, doc): + + @classmethod + def _create_action_list_node(cls, action_dict, doc): """ Create and return a xml Node from a Action list. - @param action_list A list of actions + @param action_dict Dictionary of actions with names as keys @param doc The XML document root (used to create new nodes only) @return A XML Node object with the Actions tag name and a serie of Action children """ - actionsList = doc.createElement("Actions") - for action in action_list: + actionsList = doc.createElement(ELEM_ACTIONS) + for name, action in action_dict.items(): # Create the action node - actionNode = self._create_component_node(action, doc) + actionNode = cls._create_component_node(action, doc) + actionNode.setAttribute(NAME_ATTR, name) # Append it to the list actionsList.appendChild(actionNode) return actionsList - - def _create_event_filters_node(self, event_filters, doc): - """ - Create and return a xml Node from an event filters. + + @classmethod + def _create_transitions_node(cls, transition_dict, doc): """ - eventFiltersList = doc.createElement("EventFiltersList") - for event, state in event_filters: - eventFilterNode = self._create_component_node(event, doc) - eventFilterNode.setAttribute(NEXT_STATE_ATTR, str(state)) + Create and return a xml Node from a transition dictionary. + @param transition_dict dictionary of (event, next_state) transitions. + @param doc The XML document root (used to create nodes only + @return xml Element containing the transitions + """ + eventFiltersList = doc.createElement(ELEM_TRANS) + for transition_name, (event, end_state) in transition_dict.items(): + #start_state = transition_name.split(Tutorial._NAME_SEPARATOR)[0] + #XXX The addon is not in the cache and cannot be loaded so we + # store it differently for now + if type(event) == AutomaticTransitionEvent: + eventFilterNode = doc.createElement(ELEM_AUTOTRANS) + else: + eventFilterNode = cls._create_component_node(event, doc) + #eventFilterNode.setAttribute(START_STATE_ATTR, unicode(start_state)) + eventFilterNode.setAttribute(NEXT_STATE_ATTR, unicode(end_state)) + eventFilterNode.setAttribute(NAME_ATTR, transition_name) eventFiltersList.appendChild(eventFilterNode) return eventFiltersList - def save_fsm(self, fsm, xml_filename, path): + @classmethod + def save_tutorial(cls, fsm, file_obj): """ - Save fsm to disk, in the xml file specified by "xml_filename", in the - "path" folder. If the specified file doesn't exist, it will be created. + Save fsm to file + + @param fsm Tutorial to save + @param file_obj file-like object in which the serialized fsm is saved + + Side effects: + A serialized version of the Tutorial is written to file_obj. + The file is not closed automatically. """ - self.doc = doc = xml.dom.minidom.Document() - fsm_element = doc.createElement("FSM") + doc = xml.dom.minidom.Document() + fsm_element = doc.createElement(ELEM_FSM) doc.appendChild(fsm_element) + fsm_element.setAttribute("Name", fsm.name) - fsm_element.setAttribute("StartStateName", fsm.start_state_name) - statesDict = fsm_element.appendChild(self._create_state_dict_node(fsm._states, doc)) - - fsm_actions_node = self._create_action_list_node(fsm.actions, doc) - fsm_actions_node.tagName = "FSMActions" - actionsList = fsm_element.appendChild(fsm_actions_node) - - file_object = open(os.path.join(path, xml_filename), "w") - file_object.write(doc.toprettyxml()) - file_object.close() - def _get_direct_descendants_by_tag_name(self, node, name): + states = cls._create_state_dict_node(fsm.get_state_dict(), doc) + fsm_element.appendChild(states) + + file_obj.write(doc.toprettyxml()) + + @classmethod + def _get_direct_descendants_by_tag_name(cls, node, name): """ Searches in the list of direct descendants of a node to find all the node that have the given name. @@ -597,40 +645,63 @@ class XMLSerializer(Serializer): return_list.append(childNode) return return_list - -## def _load_xml_properties(self, properties_elem): -## """ -## Changes a list of properties into fully instanciated properties. -## -## @param properties_elem An XML element reprensenting a list of -## properties -## """ -## return [] - - def _load_xml_event_filters(self, filters_elem): + @classmethod + def _load_xml_transitions(cls, filters_elem): """ Loads up a list of Event Filters. @param filters_elem An XML Element representing a list of event filters + @return dict of (event, next_state) transitions, keyed by name """ - transition_list = [] - event_filter_element_list = self._get_direct_descendants_by_tag_name(filters_elem, NODE_COMPONENT) - new_event_filter = None + transition_dict = {} + + #Retrieve normal transitions + transition_element_list = cls._get_direct_descendants_by_tag_name(filters_elem, NODE_COMPONENT) + new_transition = None - for event_filter in event_filter_element_list: - next_state = event_filter.getAttribute(NEXT_STATE_ATTR) + for transition in transition_element_list: + #start_state = transition.getAttribute(START_STATE_ATTR) + next_state = transition.getAttribute(NEXT_STATE_ATTR) + transition_name = transition.getAttribute(NAME_ATTR) try: - event_filter.removeAttribute(NEXT_STATE_ATTR) + #The attributes must be removed so that they are not + # viewed as a property in load_xml_component + # transition.removeAttribute(START_STATE_ATTR) + transition.removeAttribute(NEXT_STATE_ATTR) + transition.removeAttribute(NAME_ATTR) except NotFoundErr: - next_state = None - new_event_filter = self._load_xml_component(event_filter) + continue + + new_transition = cls._load_xml_component(transition) - if new_event_filter is not None: - transition_list.append((new_event_filter, next_state)) + if new_transition is not None: + transition_dict[transition_name] = (new_transition, next_state) + + #Retrieve automatic transitions + # XXX This is done differently as the AutomaticTransitionEvent + # cannot be loaded dynamically (yet?) + transition_element_list = cls._get_direct_descendants_by_tag_name(filters_elem, ELEM_AUTOTRANS) + new_transition = None + + for transition in transition_element_list: + #start_state = transition.getAttribute(START_STATE_ATTR) + next_state = transition.getAttribute(NEXT_STATE_ATTR) + transition_name = transition.getAttribute(NAME_ATTR) + try: + #The attributes must be removed so that they are not + # viewed as a property in load_xml_component + # transition.removeAttribute(START_STATE_ATTR) + transition.removeAttribute(NEXT_STATE_ATTR) + transition.removeAttribute(NAME_ATTR) + except NotFoundErr: + continue - return transition_list - - def _load_xml_subcomponents(self, node, properties): + transition_dict[transition_name] = (AutomaticTransitionEvent(), next_state) + + return transition_dict + + @classmethod + def _load_xml_subcomponents(cls, node, properties): """ Loads all the subcomponent node below the given node and inserts them with the right property name inside the properties dictionnary. @@ -640,15 +711,16 @@ class XMLSerializer(Serializer): and the instantiated components will be stored @returns Nothing. The properties dict will contain the property->comp mapping. """ - subCompList = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENT) + subCompList = cls._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENT) for subComp in subCompList: property_name = subComp.getAttribute("name") - internal_comp_node = self._get_direct_descendants_by_tag_name(subComp, NODE_COMPONENT)[0] - internal_comp = self._load_xml_component(internal_comp_node) + internal_comp_node = cls._get_direct_descendants_by_tag_name(subComp, NODE_COMPONENT)[0] + internal_comp = cls._load_xml_component(internal_comp_node) properties[str(property_name)] = internal_comp - def _load_xml_subcomponent_lists(self, node, properties): + @classmethod + def _load_xml_subcomponent_lists(cls, node, properties): """ Loads all the subcomponent lists below the given node and stores them under the correct property name for that node. @@ -657,16 +729,17 @@ class XMLSerializer(Serializer): @param properties The dictionnary that will contain the mapping of prop->subCompList @returns Nothing. The values are returns inside the properties dict. """ - listOf_subCompListNode = self._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENTLIST) + listOf_subCompListNode = cls._get_direct_descendants_by_tag_name(node, NODE_SUBCOMPONENTLIST) for subCompListNode in listOf_subCompListNode: property_name = subCompListNode.getAttribute("name") subCompList = [] - for subCompNode in self._get_direct_descendants_by_tag_name(subCompListNode, NODE_COMPONENT): - subComp = self._load_xml_component(subCompNode) + for subCompNode in cls._get_direct_descendants_by_tag_name(subCompListNode, NODE_COMPONENT): + subComp = cls._load_xml_component(subCompNode) subCompList.append(subComp) properties[str(property_name)] = subCompList - def _load_xml_component(self, node): + @classmethod + def _load_xml_component(cls, node): """ Loads a single addon component instance from an Xml node. @@ -685,8 +758,8 @@ class XMLSerializer(Serializer): properties[str(prop)] = eval(node.getAttribute(prop)) # Read the complex attributes - self._load_xml_subcomponents(node, properties) - self._load_xml_subcomponent_lists(node, properties) + cls._load_xml_subcomponents(node, properties) + cls._load_xml_subcomponent_lists(node, properties) new_action = addon.create(class_name, **properties) @@ -694,99 +767,88 @@ class XMLSerializer(Serializer): return None return new_action - - def _load_xml_actions(self, actions_elem): + + @classmethod + def _load_xml_actions(cls, actions_elem): """ - Transforms an Actions element into a list of instanciated Action. + Transforms an Actions element into a dict of instanciated Action. @param actions_elem An XML Element representing a list of Actions + @return dictionary of actions keyed by name """ - reformed_actions_list = [] - actions_element_list = self._get_direct_descendants_by_tag_name(actions_elem, NODE_COMPONENT) + action_dict = {} + actions_element_list = cls._get_direct_descendants_by_tag_name(actions_elem, NODE_COMPONENT) for action in actions_element_list: - new_action = self._load_xml_component(action) + action_name = action.getAttribute(NAME_ATTR) + try: + #The name attribute must be removed so that it is not + # viewed as a property in load_xml_component + action.removeAttribute(NAME_ATTR) + except NotFoundErr: + continue + new_action = cls._load_xml_component(action) - reformed_actions_list.append(new_action) + action_dict[action_name] = new_action - return reformed_actions_list - - def _load_xml_states(self, states_elem): + return action_dict + + @classmethod + def _load_xml_states(cls, states_elem): """ Takes in a States element and fleshes out a complete list of State objects. @param states_elem An XML Element that represents a list of States + @return dictionary of States """ - reformed_state_list = [] + state_dict = {} # item(0) because there is always only one tag in the xml file # so states_elem should always contain only one element - states_element_list = states_elem.item(0).getElementsByTagName("State") + states_element_list = states_elem.item(0).getElementsByTagName(ELEM_STATE) for state in states_element_list: 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 = self._load_xml_actions(state.getElementsByTagName("Actions")[0]) - event_filters_list = self._load_xml_event_filters(state.getElementsByTagName("EventFiltersList")[0]) - reformed_state_list.append(State(stateName, actions_list, event_filters_list)) + actions_list = 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) - return reformed_state_list + return state_dict - def load_xml_fsm(self, fsm_elem): + @classmethod + def load_xml_tutorial(cls, fsm_elem): """ Takes in an XML element representing an FSM and returns the fully crafted FSM. @param fsm_elem The XML element that describes a FSM + @return Tutorial loaded from xml element """ # Load the FSM's name and start state's name fsm_name = fsm_elem.getAttribute("Name") - fsm_start_state_name = None - try: - fsm_start_state_name = fsm_elem.getAttribute("StartStateName") - except: - pass - - fsm = FiniteStateMachine(fsm_name, start_state_name=fsm_start_state_name) - # Load the states - states = self._load_xml_states(fsm_elem.getElementsByTagName("States")) - for state in states: - fsm.add_state(state) - - # Load the actions on this FSM - actions = self._load_xml_actions(fsm_elem.getElementsByTagName("FSMActions")[0]) - for action in actions: - fsm.add_action(action) - - # Load the event filters - events = self._load_xml_event_filters(fsm_elem.getElementsByTagName("EventFiltersList")[0]) - for event, next_state in events: - fsm.add_event_filter(event, next_state) - - return fsm + states_dict = cls._load_xml_states(fsm_elem.getElementsByTagName(ELEM_STATES)) + fsm = Tutorial(fsm_name, states_dict) - - def load_fsm(self, guid, path=None): + return fsm + + @classmethod + def load_tutorial(cls, tutorial_file): """ - Load fsm from xml file whose .ini file guid match argument guid. + Load fsm from xml file + @param tutorial_file file-like object to read the fsm from + @return Tutorial object that was loaded from the file """ - # Fetch the directory (if any) - bundler = TutorialBundler(guid) - tutorial_dir = bundler.get_tutorial_path(guid) - - # Open the XML file - tutorial_file = os.path.join(tutorial_dir, TUTORIAL_FILENAME) - xml_dom = xml.dom.minidom.parse(tutorial_file) - fsm_elem = xml_dom.getElementsByTagName("FSM")[0] + fsm_elem = xml_dom.getElementsByTagName(ELEM_FSM)[0] - return self.load_xml_fsm(fsm_elem) - - + return cls.load_xml_tutorial(fsm_elem) + class TutorialBundler(object): """ This class provide the various data handling methods useable by the tutorial @@ -919,11 +981,11 @@ class TutorialBundler(object): path = os.path.join(self.Path, "meta.ini") config.read(path) xml_filename = config.get(INI_METADATA_SECTION, INI_XML_FSM_PROPERTY) - serializer.save_fsm(fsm, xml_filename, self.Path) + serializer.save_tutorial(fsm, xml_filename, self.Path) @staticmethod def add_resources(typename, file): """ - Add ressources to metadata. + Add resources to metadata. """ raise NotImplementedError("add_resources not implemented") -- cgit v0.9.1