Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--tests/vaulttests.py189
-rw-r--r--tutorius/tutorial.py55
-rw-r--r--tutorius/vault.py343
3 files changed, 303 insertions, 284 deletions
diff --git a/tests/vaulttests.py b/tests/vaulttests.py
index 02c34e8..6d69ca9 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,29 +246,19 @@ 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, \
- '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"
# 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 tearDown(self):
folder = os.path.join(os.getenv("HOME"),".sugar", 'default', 'tutorius', 'data');
for file in os.listdir(folder):
@@ -298,8 +278,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
@@ -307,8 +287,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
@@ -318,100 +298,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):
- 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')
@@ -423,26 +364,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")
@@ -450,20 +389,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):
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 b455a52..d6b4720 100644
--- a/tutorius/vault.py
+++ b/tutorius/vault.py
@@ -31,7 +31,7 @@ import zipfile
from ConfigParser import SafeConfigParser
from . import addon
-from .core import Tutorial, State, FiniteStateMachine
+from .tutorial import Tutorial, State, AutomaticTransitionEvent
logger = logging.getLogger("tutorius")
@@ -58,10 +58,22 @@ INI_XML_FSM_PROPERTY = "fsm_filename"
INI_VERSION_PROPERTY = 'version'
INI_FILENAME = "meta.ini"
TUTORIAL_FILENAME = "tutorial.xml"
+
+######################################################################
+# 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"
+NAME_ATTR = "__name__"
+NEXT_STATE_ATTR = "__next_state__"
+START_STATE_ATTR = "__start_state__"
class Vault(object):
@@ -242,7 +254,8 @@ class Vault(object):
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)
@@ -253,15 +266,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
"""
@@ -275,7 +293,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")
@@ -327,7 +347,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
@@ -337,7 +357,7 @@ class Serializer(object):
"""
raise NotImplementedError()
- def load_fsm(self):
+ def load_tutorial(self):
"""
Load fsm from disk.
"""
@@ -348,21 +368,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.
@@ -389,13 +414,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
@@ -422,12 +448,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.
@@ -446,68 +473,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.
@@ -528,40 +573,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.
@@ -571,15 +639,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.
@@ -588,16 +657,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.
@@ -616,8 +686,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)
@@ -625,99 +695,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 <States> 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
@@ -850,7 +909,7 @@ 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):