Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--addons/EmbeddedInterpreter.py30
-rw-r--r--addons/WidgetIdentifier.py35
-rw-r--r--addons/eventgenerator.py60
-rw-r--r--tests/constraintstests.py42
-rw-r--r--tests/inject.py57
-rw-r--r--tests/propertiestests.py57
-rw-r--r--tests/translatortests.py91
-rw-r--r--tests/vaulttests.py192
-rw-r--r--tutorius/actions.py1
-rw-r--r--tutorius/constraints.py39
-rw-r--r--tutorius/core.py22
-rw-r--r--tutorius/editor_interpreter.py105
-rw-r--r--tutorius/events.py36
-rw-r--r--tutorius/ipython_view.py301
-rw-r--r--tutorius/properties.py38
-rw-r--r--tutorius/translator.py195
-rw-r--r--tutorius/tutorial.py55
-rw-r--r--tutorius/vault.py349
18 files changed, 1412 insertions, 293 deletions
diff --git a/addons/EmbeddedInterpreter.py b/addons/EmbeddedInterpreter.py
new file mode 100644
index 0000000..8c3522e
--- /dev/null
+++ b/addons/EmbeddedInterpreter.py
@@ -0,0 +1,30 @@
+from sugar.tutorius.actions import Action
+from sugar.tutorius.editor_interpreter import EditorInterpreter
+from sugar.tutorius.services import ObjectStore
+
+class EmbeddedInterpreter(Action):
+ def __init__(self):
+ Action.__init__(self)
+ self.activity = None
+ self._dialog = None
+
+ def do(self):
+ os = ObjectStore()
+ if os.activity:
+ self.activity = os.activity
+
+ self._dialog = EditorInterpreter(self.activity)
+ self._dialog.show()
+
+
+ def undo(self):
+ if self._dialog:
+ self._dialog.destroy()
+
+__action__ = {
+ "name" : "EmbeddedInterpreter",
+ "display_name" : "Embedded Interpreter",
+ "icon" : "message-bubble",
+ "class" : EmbeddedInterpreter,
+ "mandatory_props" : []
+}
diff --git a/addons/WidgetIdentifier.py b/addons/WidgetIdentifier.py
new file mode 100644
index 0000000..3c559b5
--- /dev/null
+++ b/addons/WidgetIdentifier.py
@@ -0,0 +1,35 @@
+from sugar.tutorius.actions import Action
+from sugar.tutorius.editor import WidgetIdentifier as WIPrimitive
+from sugar.tutorius.services import ObjectStore
+
+class WidgetIdentifier(Action):
+ def __init__(self):
+ Action.__init__(self)
+ self.activity = None
+ self._dialog = None
+
+ def do(self):
+ os = ObjectStore()
+ if os.activity:
+ self.activity = os.activity
+
+ self._dialog = WIPrimitive(self.activity)
+ self._dialog.show()
+
+
+ def undo(self):
+ if self._dialog:
+ # TODO elavoie 2009-07-19
+ # We should disconnect the handlers, however there seems to be an error
+ # saying that the size of the dictionary changed during the iteration
+ # We should investigate this
+ #self._dialog._disconnect_handlers()
+ self._dialog.destroy()
+
+__action__ = {
+ "name" : "WidgetIdentifier",
+ "display_name" : "Widget Identifier",
+ "icon" : "message-bubble",
+ "class" : WidgetIdentifier,
+ "mandatory_props" : []
+}
diff --git a/addons/eventgenerator.py b/addons/eventgenerator.py
new file mode 100644
index 0000000..a91ccf4
--- /dev/null
+++ b/addons/eventgenerator.py
@@ -0,0 +1,60 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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
+from sugar.tutorius.actions import *
+from sugar.tutorius.gtkutils import find_widget
+
+class EventGenerator(Action):
+ source = TUAMProperty("")
+ type = TStringProperty("clicked")
+
+ def __init__(self, source=None, type="clicked"):
+ Action.__init__(self)
+
+ if source != None:
+ self.source = source
+
+ if type != "clicked":
+ self.type = type
+
+ def do(self):
+ self._activity = ObjectStore().activity
+
+ # TODO elavoie 2009-07-25 We should eventually use the UAM mecanism
+ widget = find_widget(self._activity, self.source)
+
+
+ # TODO elavoie 2009-07-25 We assume a gtk activity, it might
+ # get messier with other widget systems
+
+ # Call the signal on the widget
+ # We use introspection here to obtain the
+ # method that will send the corresponding
+ # signal on the gtk object
+ getattr(widget, self.type)()
+
+ # That's all!!!
+
+ def undo(self):
+ pass
+
+__action__ = {
+ "name" : "EventGenerator",
+ "display_name" : "Event Generator",
+ "icon" : "message-bubble",
+ "class" : EventGenerator,
+ "mandatory_props" : ["source", "type"]
+}
+
diff --git a/tests/constraintstests.py b/tests/constraintstests.py
index 4e19a92..a5ccf26 100644
--- a/tests/constraintstests.py
+++ b/tests/constraintstests.py
@@ -240,5 +240,47 @@ class FileConstraintTest(unittest.TestCase):
except FileConstraintError:
pass
+class ResourceConstraintTest(unittest.TestCase):
+ def test_valid_names(self):
+ name1 = "file_" + unicode(uuid.uuid1()) + ".png"
+ name2 = unicode(uuid.uuid1()) + "_" + unicode(uuid.uuid1()) + ".extension"
+ name3 = "/home/user/.sugar/_random/new_image1231_" + unicode(uuid.uuid1()).upper() + ".mp3"
+ name4 = "a_" + unicode(uuid.uuid1())
+ name5 = ""
+
+ cons = ResourceConstraint()
+
+ # All of those names should pass without exceptions
+ cons.validate(name1)
+ cons.validate(name2)
+ cons.validate(name3)
+ cons.validate(name4)
+ cons.validate(name5)
+
+ def test_invalid_names(self):
+ bad_name1 = ".jpg"
+ bad_name2 = "_.jpg"
+ bad_name3 = "_" + unicode(uuid.uuid1())
+
+ cons = ResourceConstraint()
+
+ try:
+ cons.validate(bad_name1)
+ assert False, "%s should not be a valid resource name" % bad_name1
+ except ResourceConstraintError:
+ pass
+
+ try:
+ cons.validate(bad_name2)
+ assert False, "%s should not be a valid resource name" % bad_name2
+ except ResourceConstraintError:
+ pass
+
+ try:
+ cons.validate(bad_name3)
+ assert False, "%s should not be a valid resource name" % bad_name3
+ except ResourceConstraintError:
+ pass
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/inject.py b/tests/inject.py
new file mode 100644
index 0000000..d69d6ff
--- /dev/null
+++ b/tests/inject.py
@@ -0,0 +1,57 @@
+#Test event injection
+
+import gtk
+import gobject
+import time
+import types
+
+class ClickMaster():
+ def __init__(self):
+ self.event = None
+
+ def connect(self, button):
+ self.id = button.connect("pressed",self.capture_event)
+ self.id2 = button.connect("released",self.capture_event2)
+ self.id3 = button.connect("clicked",self.capture_event3)
+ self.button = button
+
+ def capture_event(self, *args):
+ print "Capture Event"
+ print args
+ self.eventPress = args[-1]
+ return False
+
+ def capture_event2(self, *args):
+ print "Capture Release"
+ print args
+ self.eventReleased = args[-1]
+ return False
+
+ def capture_event3(self, *args):
+ print "Capture Clicked"
+ print args
+ self.eventClicked = args[-1]
+ return False
+
+ def inject_event(self):
+ print "Injecting"
+ print self.event
+ #self.event.put()
+ self.button.emit("button_press_event", self.event)
+
+def print_Event(event):
+ for att in dir(event):
+ if not isinstance(att, types.MethodType):
+ print att, getattr(event, att)
+
+if __name__=='__main__':
+ w = gtk.Window()
+ b = gtk.CheckButton("Auto toggle!")
+ c=ClickMaster()
+ w.add(b)
+ b.show()
+ c.connect(b)
+
+ w.show()
+
+ gtk.main()
diff --git a/tests/propertiestests.py b/tests/propertiestests.py
index 2494ea6..f07bd43 100644
--- a/tests/propertiestests.py
+++ b/tests/propertiestests.py
@@ -579,6 +579,63 @@ class TAddonPropertyList(unittest.TestCase):
obj1.addonlist = [klass1(), klass1(), wrongAddon(), klass1()]
except ValueError:
pass
+
+class TResourcePropertyTest(unittest.TestCase):
+ def test_valid_names(self):
+ class klass1(TPropContainer):
+ res = TResourceProperty()
+
+ name1 = "file_" + unicode(uuid.uuid1()) + ".png"
+ name2 = unicode(uuid.uuid1()) + "_" + unicode(uuid.uuid1()) + ".extension"
+ name3 = "/home/user/.sugar/_random/new_image1231_" + unicode(uuid.uuid1()).upper() + ".mp3"
+ name4 = "a_" + unicode(uuid.uuid1())
+ name5 = ""
+
+ obj1 = klass1()
+
+ obj1.res = name1
+ assert obj1.res == name1, "Could not assign the valid name correctly : %s" % name1
+
+ obj1.res = name2
+ assert obj1.res == name2, "Could not assign the valid name correctly : %s" % name2
+
+ obj1.res = name3
+ assert obj1.res == name3, "Could not assign the valid name correctly : %s" % name3
+
+ obj1.res = name4
+ assert obj1.res == name4, "Could not assign the valid name correctly : %s" % name4
+
+ obj1.res = name5
+ assert obj1.res == name5, "Could not assign the valid name correctly : %s" % name5
+
+ def test_invalid_names(self):
+ class klass1(TPropContainer):
+ res = TResourceProperty()
+
+ bad_name1 = ".jpg"
+ bad_name2 = "_.jpg"
+ bad_name3 = "_" + unicode(uuid.uuid1())
+
+ obj1 = klass1()
+
+ try:
+ obj1.res = bad_name1
+ assert False, "A invalid name was accepted : %s" % bad_name1
+ except ResourceConstraintError:
+ pass
+
+ try:
+ obj1.res = bad_name2
+ assert False, "A invalid name was accepted : %s" % bad_name2
+ except ResourceConstraintError:
+ pass
+
+ try:
+ obj1.res = bad_name3
+ assert False, "A invalid name was accepted : %s" % bad_name3
+ except ResourceConstraintError:
+ pass
+
if __name__ == "__main__":
unittest.main()
diff --git a/tests/translatortests.py b/tests/translatortests.py
new file mode 100644
index 0000000..0f053b9
--- /dev/null
+++ b/tests/translatortests.py
@@ -0,0 +1,91 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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
+
+import unittest
+import os
+
+from sugar.tutorius.translator import *
+from sugar.tutorius.properties import *
+
+##############################################################################
+## Helper classes
+class ResourceAction(TPropContainer):
+ resource = TResourceProperty()
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+
+class NestedResource(TPropContainer):
+ nested = TAddonProperty()
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+ self.nested = ResourceAction()
+
+class ListResources(TPropContainer):
+ nested_list = TAddonListProperty()
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+ self.nested_list = [ResourceAction(), ResourceAction()]
+
+##
+##############################################################################
+
+class ResourceTranslatorTests(unittest.TestCase):
+ temp_path = "/tmp/tutorius"
+ file_name = "temp.txt"
+
+ def setUp(self):
+ try:
+ os.mkdir(self.temp_path)
+ except:
+ pass
+ new_file = file(os.path.join(self.temp_path, self.file_name), "w")
+
+ # Use a dummy prob manager - we shouldn't be using it
+ self.prob_man = object()
+
+ self.translator = ResourceTranslator(self.prob_man, '0101010101', testing=True)
+
+ pass
+
+ def tearDown(self):
+ os.unlink(os.path.join(self.temp_path, self.file_name))
+ pass
+
+ def test_translate(self):
+ # Create an action with a resource property
+ res_action = ResourceAction()
+
+ self.translator.translate(res_action)
+
+ assert getattr(res_action, "resource").type == "file", "Resource was not converted to file"
+
+ def test_recursive_translate(self):
+ nested_action = NestedResource()
+
+ self.translator.translate(nested_action)
+
+ assert getattr(getattr(nested_action, "nested"), "resource").type == "file", "Nested resource was not converted properly"
+
+ def test_list_translate(self):
+ list_action = ListResources()
+
+ self.translator.translate(list_action)
+
+ for container in list_action.nested_list:
+ assert getattr(container, "resource").type == "file", "Element of list was not converted properly"
diff --git a/tests/vaulttests.py b/tests/vaulttests.py
index d6787ef..48869a7 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,23 +246,15 @@ 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
@@ -337,8 +319,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 +328,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 +339,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 +405,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 +430,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):
@@ -555,4 +491,4 @@ class TutorialBundlerTests(unittest.TestCase):
if __name__ == "__main__":
- unittest.main() \ No newline at end of file
+ unittest.main()
diff --git a/tutorius/actions.py b/tutorius/actions.py
index bb15459..d5a8641 100644
--- a/tutorius/actions.py
+++ b/tutorius/actions.py
@@ -81,6 +81,7 @@ class DragWrapper(object):
"""Callback for end of drag (stolen focus)."""
self._dragging = False
+
def set_draggable(self, value):
"""Setter for the draggable property"""
if bool(value) ^ bool(self._drag_on):
diff --git a/tutorius/constraints.py b/tutorius/constraints.py
index 519bce8..cd71167 100644
--- a/tutorius/constraints.py
+++ b/tutorius/constraints.py
@@ -24,6 +24,8 @@ for some properties.
# For the File Constraint
import os
+# For the Resource Constraint
+import re
class ConstraintException(Exception):
"""
@@ -214,3 +216,40 @@ class FileConstraint(Constraint):
raise FileConstraintError("Non-existing file : %s"%value)
return
+class ResourceConstraintError(ConstraintException):
+ pass
+
+class ResourceConstraint(Constraint):
+ """
+ Ensures that the value is looking like a resource name, like
+ <filename>_<GUID>[.<extension>]. We are not validating that this is a
+ valid resource for the reason that the property does not have any notion
+ of tutorial guid.
+
+ TODO : Find a way to properly validate resources by looking them up in the
+ Vault.
+ """
+
+ # Regular expression to parse a resource-like name
+ resource_regexp_text = "(.+)_([a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})(\..*)?$"
+ resource_regexp = re.compile(resource_regexp_text)
+
+ def validate(self, value):
+ # TODO : Validate that we will not use an empty resource or if we can
+ # have transitory resource names
+ if value is None:
+ raise ResourceConstraintError("Resource not allowed to have a null value!")
+
+ # Special case : We allow the empty resource name for now
+ if value == "":
+ return value
+
+ # Attempt to see if the value has a resource name inside it
+ match = self.resource_regexp.search(value)
+
+ # If there was no match on the reg exp
+ if not match:
+ raise ResourceConstraintError("Resource name does not seem to be valid : %s" % value)
+
+ # If the name matched, then the value is _PROBABLY_ good
+ return value
diff --git a/tutorius/core.py b/tutorius/core.py
index bfbe07b..80e1b4f 100644
--- a/tutorius/core.py
+++ b/tutorius/core.py
@@ -616,3 +616,25 @@ class FiniteStateMachine(State):
# 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/editor_interpreter.py b/tutorius/editor_interpreter.py
new file mode 100644
index 0000000..d559266
--- /dev/null
+++ b/tutorius/editor_interpreter.py
@@ -0,0 +1,105 @@
+# Copyright (C) 2009, Tutorius.org
+# Greatly influenced by sugar/activity/namingalert.py
+#
+# 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
+""" Tutorial Editor Interpreter Module
+"""
+
+import gtk
+import pango
+from sugar.tutorius.ipython_view import *
+
+from gettext import gettext as _
+
+class EditorInterpreter(gtk.Window):
+ """
+ Interpreter that will be shown to the user
+ """
+ __gtype_name__ = 'TutoriusEditorInterpreter'
+
+ def __init__(self, activity=None):
+ gtk.Window.__init__(self)
+
+ self._activity = activity
+
+ # Set basic properties of window
+ self.set_decorated(False)
+ self.set_resizable(False)
+ self.set_modal(False)
+
+ # Connect to realize signal from ?
+ self.connect('realize', self.__realize_cb)
+
+ # Use an expander widget to allow minimizing
+ self._expander = gtk.Expander(_("Editor Interpreter"))
+ self._expander.set_expanded(True)
+ self.add(self._expander)
+ self._expander.connect("notify::expanded", self.__expander_cb)
+
+
+ # Use the IPython widget to embed
+ self.interpreter = IPythonView()
+ self.interpreter.set_wrap_mode(gtk.WRAP_CHAR)
+
+ # Expose the activity object in the interpreter
+ self.interpreter.updateNamespace({'activity':self._activity})
+
+ # Use a scroll window to permit scrolling of the interpreter prompt
+ swd = gtk.ScrolledWindow()
+ swd.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
+ swd.add(self.interpreter)
+ self.interpreter.show()
+
+ # Notify GTK that expander is ready
+ self._expander.add(swd)
+ self._expander.show()
+
+ # Notify GTK that the scrolling window is ready
+ swd.show()
+
+ def __expander_cb(self, *args):
+ """Callback for the window expander toggling"""
+ if self._expander.get_expanded():
+ self.__move_expanded()
+ else:
+ self.__move_collapsed()
+
+ def __move_expanded(self):
+ """Move the window to it's expanded position"""
+ swidth = gtk.gdk.screen_width()
+ sheight = gtk.gdk.screen_height()
+ # leave room for the scrollbar at the right
+ width = swidth - 20
+ height = 200
+
+ self.set_size_request(width, height)
+ # Put at the bottom of the screen
+ self.move(0, sheight-height)
+
+ def __move_collapsed(self):
+ """Move the window to it's collapsed position"""
+ width = 150
+ height = 40
+ swidth = gtk.gdk.screen_width()
+ sheight = gtk.gdk.screen_height()
+
+ self.set_size_request(width, height)
+ self.move(((swidth-width)/2)-150, sheight-height)
+
+ def __realize_cb(self, widget):
+ """Callback for realize"""
+ self.window.set_type_hint(gtk.gdk.WINDOW_TYPE_HINT_DIALOG)
+ self.window.set_accept_focus(True)
+ self.__move_expanded()
diff --git a/tutorius/events.py b/tutorius/events.py
new file mode 100644
index 0000000..bf0a8b9
--- /dev/null
+++ b/tutorius/events.py
@@ -0,0 +1,36 @@
+from sugar.tutorius.properties import *
+
+class Event(TPropContainer):
+ source = TUAMProperty()
+ type = TStringProperty("clicked")
+
+ def __init__(self):
+ TPropContainer.__init__(self)
+
+
+ # Providing the hash methods necessary to use events as key
+ # in a dictionary, if new properties are added we should
+ # take them into account here
+ def __hash__(self):
+ return hash(str(self.source) + str(self.type))
+
+ def __eq__(self, e2):
+ return self.source == e2.source and self.type == e2.type
+
+
+ # Adding methods for pickling and unpickling an object with
+ # properties
+ def __getstate__(self):
+ return self._props.copy()
+
+ def __setstate__(self, dict):
+ self._props.update(dict)
+
+
+# Nothing more needs to be added, the additional
+# information is in the object type
+class ClickedEvent(Event):
+ def __init__(self):
+ Event.__init__(self)
+ self.type = "clicked"
+
diff --git a/tutorius/ipython_view.py b/tutorius/ipython_view.py
new file mode 100644
index 0000000..c4294d0
--- /dev/null
+++ b/tutorius/ipython_view.py
@@ -0,0 +1,301 @@
+"""
+Backend to the console plugin.
+
+@author: Eitan Isaacson
+@organization: IBM Corporation
+@copyright: Copyright (c) 2007 IBM Corporation
+@license: BSD
+
+All rights reserved. This program and the accompanying materials are made
+available under the terms of the BSD which accompanies this distribution, and
+is available at U{http://www.opensource.org/licenses/bsd-license.php}
+"""
+# this file is a modified version of source code from the Accerciser project
+# http://live.gnome.org/accerciser
+
+import gtk
+import re
+import sys
+import os
+import pango
+from StringIO import StringIO
+
+try:
+ import IPython
+except Exception,e:
+ raise "Error importing IPython (%s)" % str(e)
+
+ansi_colors = {'0;30': 'Black',
+ '0;31': 'Red',
+ '0;32': 'Green',
+ '0;33': 'Brown',
+ '0;34': 'Blue',
+ '0;35': 'Purple',
+ '0;36': 'Cyan',
+ '0;37': 'LightGray',
+ '1;30': 'DarkGray',
+ '1;31': 'DarkRed',
+ '1;32': 'SeaGreen',
+ '1;33': 'Yellow',
+ '1;34': 'LightBlue',
+ '1;35': 'MediumPurple',
+ '1;36': 'LightCyan',
+ '1;37': 'White'}
+
+class IterableIPShell:
+ def __init__(self,argv=None,user_ns=None,user_global_ns=None,
+ cin=None, cout=None,cerr=None, input_func=None):
+ if input_func:
+ IPython.iplib.raw_input_original = input_func
+ if cin:
+ IPython.Shell.Term.cin = cin
+ if cout:
+ IPython.Shell.Term.cout = cout
+ if cerr:
+ IPython.Shell.Term.cerr = cerr
+
+ if argv is None:
+ argv=[]
+
+ # This is to get rid of the blockage that occurs during
+ # IPython.Shell.InteractiveShell.user_setup()
+ IPython.iplib.raw_input = lambda x: None
+
+ self.term = IPython.genutils.IOTerm(cin=cin, cout=cout, cerr=cerr)
+ os.environ['TERM'] = 'dumb'
+ excepthook = sys.excepthook
+ self.IP = IPython.Shell.make_IPython(argv,user_ns=user_ns,
+ user_global_ns=user_global_ns,
+ embedded=True,
+ shell_class=IPython.Shell.InteractiveShell)
+ self.IP.system = lambda cmd: self.shell(self.IP.var_expand(cmd),
+ header='IPython system call: ',
+ verbose=self.IP.rc.system_verbose)
+ sys.excepthook = excepthook
+ self.iter_more = 0
+ self.history_level = 0
+ self.complete_sep = re.compile('[\s\{\}\[\]\(\)]')
+
+ def execute(self):
+ self.history_level = 0
+ orig_stdout = sys.stdout
+ sys.stdout = IPython.Shell.Term.cout
+ try:
+ line = self.IP.raw_input(None, self.iter_more)
+ if self.IP.autoindent:
+ self.IP.readline_startup_hook(None)
+ except KeyboardInterrupt:
+ self.IP.write('\nKeyboardInterrupt\n')
+ self.IP.resetbuffer()
+ # keep cache in sync with the prompt counter:
+ self.IP.outputcache.prompt_count -= 1
+
+ if self.IP.autoindent:
+ self.IP.indent_current_nsp = 0
+ self.iter_more = 0
+ except:
+ self.IP.showtraceback()
+ else:
+ self.iter_more = self.IP.push(line)
+ if (self.IP.SyntaxTB.last_syntax_error and
+ self.IP.rc.autoedit_syntax):
+ self.IP.edit_syntax_error()
+ if self.iter_more:
+ self.prompt = str(self.IP.outputcache.prompt2).strip()
+ if self.IP.autoindent:
+ self.IP.readline_startup_hook(self.IP.pre_readline)
+ else:
+ self.prompt = str(self.IP.outputcache.prompt1).strip()
+ sys.stdout = orig_stdout
+
+ def historyBack(self):
+ self.history_level -= 1
+ return self._getHistory()
+
+ def historyForward(self):
+ self.history_level += 1
+ return self._getHistory()
+
+ def _getHistory(self):
+ try:
+ rv = self.IP.user_ns['In'][self.history_level].strip('\n')
+ except IndexError:
+ self.history_level = 0
+ rv = ''
+ return rv
+
+ def updateNamespace(self, ns_dict):
+ self.IP.user_ns.update(ns_dict)
+
+ def complete(self, line):
+ split_line = self.complete_sep.split(line)
+ possibilities = self.IP.complete(split_line[-1])
+ if possibilities:
+ common_prefix = reduce(self._commonPrefix, possibilities)
+ completed = line[:-len(split_line[-1])]+common_prefix
+ else:
+ completed = line
+ return completed, possibilities
+
+ def _commonPrefix(self, str1, str2):
+ for i in range(len(str1)):
+ if not str2.startswith(str1[:i+1]):
+ return str1[:i]
+ return str1
+
+ def shell(self, cmd,verbose=0,debug=0,header=''):
+ stat = 0
+ if verbose or debug: print header+cmd
+ # flush stdout so we don't mangle python's buffering
+ if not debug:
+ input, output = os.popen4(cmd)
+ print output.read()
+ output.close()
+ input.close()
+
+class ConsoleView(gtk.TextView):
+ def __init__(self):
+ gtk.TextView.__init__(self)
+ self.modify_font(pango.FontDescription('Mono'))
+ self.set_cursor_visible(True)
+ self.text_buffer = self.get_buffer()
+ self.mark = self.text_buffer.create_mark('scroll_mark',
+ self.text_buffer.get_end_iter(),
+ False)
+ for code in ansi_colors:
+ self.text_buffer.create_tag(code,
+ foreground=ansi_colors[code],
+ weight=700)
+ self.text_buffer.create_tag('0')
+ self.text_buffer.create_tag('notouch', editable=False)
+ self.color_pat = re.compile('\x01?\x1b\[(.*?)m\x02?')
+ self.line_start = \
+ self.text_buffer.create_mark('line_start',
+ self.text_buffer.get_end_iter(), True
+ )
+ self.connect('key-press-event', self._onKeypress)
+ self.last_cursor_pos = 0
+
+ def write(self, text, editable=False):
+ segments = self.color_pat.split(text)
+ segment = segments.pop(0)
+ start_mark = self.text_buffer.create_mark(None,
+ self.text_buffer.get_end_iter(),
+ True)
+ self.text_buffer.insert(self.text_buffer.get_end_iter(), segment)
+
+ if segments:
+ ansi_tags = self.color_pat.findall(text)
+ for tag in ansi_tags:
+ i = segments.index(tag)
+ self.text_buffer.insert_with_tags_by_name(self.text_buffer.get_end_iter(),
+ segments[i+1], tag)
+ segments.pop(i)
+ if not editable:
+ self.text_buffer.apply_tag_by_name('notouch',
+ self.text_buffer.get_iter_at_mark(start_mark),
+ self.text_buffer.get_end_iter())
+ self.text_buffer.delete_mark(start_mark)
+ self.scroll_mark_onscreen(self.mark)
+
+ def showPrompt(self, prompt):
+ self.write(prompt)
+ self.text_buffer.move_mark(self.line_start,self.text_buffer.get_end_iter())
+
+ def changeLine(self, text):
+ iter = self.text_buffer.get_iter_at_mark(self.line_start)
+ iter.forward_to_line_end()
+ self.text_buffer.delete(self.text_buffer.get_iter_at_mark(self.line_start), iter)
+ self.write(text, True)
+
+ def getCurrentLine(self):
+ rv = self.text_buffer.get_slice(self.text_buffer.get_iter_at_mark(self.line_start),
+ self.text_buffer.get_end_iter(), False)
+ return rv
+
+ def showReturned(self, text):
+ iter = self.text_buffer.get_iter_at_mark(self.line_start)
+ iter.forward_to_line_end()
+ self.text_buffer.apply_tag_by_name('notouch',
+ self.text_buffer.get_iter_at_mark(self.line_start),
+ iter)
+ self.write('\n'+text)
+ if text:
+ self.write('\n')
+ self.showPrompt(self.prompt)
+ self.text_buffer.move_mark(self.line_start,self.text_buffer.get_end_iter())
+ self.text_buffer.place_cursor(self.text_buffer.get_end_iter())
+
+ def _onKeypress(self, obj, event):
+ if not event.string:
+ return
+ insert_mark = self.text_buffer.get_insert()
+ insert_iter = self.text_buffer.get_iter_at_mark(insert_mark)
+ selection_mark = self.text_buffer.get_selection_bound()
+ selection_iter = self.text_buffer.get_iter_at_mark(selection_mark)
+ start_iter = self.text_buffer.get_iter_at_mark(self.line_start)
+ if start_iter.compare(insert_iter) <= 0 and \
+ start_iter.compare(selection_iter) <= 0:
+ return
+ elif start_iter.compare(insert_iter) > 0 and \
+ start_iter.compare(selection_iter) > 0:
+ self.text_buffer.place_cursor(start_iter)
+ elif insert_iter.compare(selection_iter) < 0:
+ self.text_buffer.move_mark(insert_mark, start_iter)
+ elif insert_iter.compare(selection_iter) > 0:
+ self.text_buffer.move_mark(selection_mark, start_iter)
+
+
+class IPythonView(ConsoleView, IterableIPShell):
+ def __init__(self):
+ ConsoleView.__init__(self)
+ self.cout = StringIO()
+ IterableIPShell.__init__(self, cout=self.cout,cerr=self.cout,
+ input_func=self.raw_input)
+ self.connect('key_press_event', self.keyPress)
+ self.execute()
+ self.cout.truncate(0)
+ self.showPrompt(self.prompt)
+ self.interrupt = False
+
+ def raw_input(self, prompt=''):
+ if self.interrupt:
+ self.interrupt = False
+ raise KeyboardInterrupt
+ return self.getCurrentLine()
+
+ def keyPress(self, widget, event):
+ if event.state & gtk.gdk.CONTROL_MASK and event.keyval == 99:
+ self.interrupt = True
+ self._processLine()
+ return True
+ elif event.keyval == gtk.keysyms.Return:
+ self._processLine()
+ return True
+ elif event.keyval == gtk.keysyms.Up:
+ self.changeLine(self.historyBack())
+ return True
+ elif event.keyval == gtk.keysyms.Down:
+ self.changeLine(self.historyForward())
+ return True
+ elif event.keyval == gtk.keysyms.Tab:
+ if not self.getCurrentLine().strip():
+ return False
+ completed, possibilities = self.complete(self.getCurrentLine())
+ if len(possibilities) > 1:
+ slice = self.getCurrentLine()
+ self.write('\n')
+ for symbol in possibilities:
+ self.write(symbol+'\n')
+ self.showPrompt(self.prompt)
+ self.changeLine(completed or slice)
+ return True
+
+ def _processLine(self):
+ self.history_pos = 0
+ self.execute()
+ rv = self.cout.getvalue()
+ if rv: rv = rv.strip('\n')
+ self.showReturned(rv)
+ self.cout.truncate(0)
+
diff --git a/tutorius/properties.py b/tutorius/properties.py
index 5422532..ba3c211 100644
--- a/tutorius/properties.py
+++ b/tutorius/properties.py
@@ -24,7 +24,8 @@ from copy import copy, deepcopy
from .constraints import Constraint, \
UpperLimitConstraint, LowerLimitConstraint, \
MaxSizeConstraint, MinSizeConstraint, \
- ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint
+ ColorConstraint, FileConstraint, BooleanConstraint, EnumConstraint, \
+ ResourceConstraint
class TPropContainer(object):
"""
@@ -261,8 +262,6 @@ class TFileProperty(TutoriusProperty):
For now, the path may be relative or absolute, as long as it exists on
the local machine.
- TODO : Make sure that we have a file scheme that supports distribution
- on other computers (LP 355197)
"""
TutoriusProperty.__init__(self)
@@ -351,7 +350,7 @@ class TEventType(TutoriusProperty):
class TAddonListProperty(TutoriusProperty):
"""
- Reprensents an embedded tutorius Addon List Component.
+ Represents an embedded tutorius Addon List Component.
See TAddonProperty
"""
def __init__(self):
@@ -367,3 +366,34 @@ class TAddonListProperty(TutoriusProperty):
return value
raise ValueError("Value proposed to TAddonListProperty is not a list")
+class TResourceProperty(TutoriusProperty):
+ """
+ Represents a resource in the tutorial. A resource is a file with a specific
+ name that exists under the tutorials folder. It is distributed alongside the
+ tutorial itself.
+
+ When the system encounters a resource, it knows that it refers to a file in
+ the resource folder and that it should translate this resource name to an
+ absolute file name before it is executed.
+
+ E.g. An image is added to a tutorial in an action. On doing so, the creator
+ adds a resource to the tutorial, then saves its name in the resource
+ property of that action. When this tutorial is executed, the Engine
+ replaces all the TResourceProperties inside the action by their equivalent
+ TFileProperties with absolute paths, so that they can be used on any
+ machine.
+ """
+ def __init__(self, resource_name=""):
+ """
+ Creates a new resource pointing to an existing resource.
+
+ @param resource_name The file name of the resource (should be only the
+ file name itself, no directory information)
+ """
+ TutoriusProperty.__init__(self)
+ self.type = "resource"
+
+ self.resource_cons = ResourceConstraint()
+
+ self.default = self.validate("")
+
diff --git a/tutorius/translator.py b/tutorius/translator.py
new file mode 100644
index 0000000..9925346
--- /dev/null
+++ b/tutorius/translator.py
@@ -0,0 +1,195 @@
+# Copyright (C) 2009, Tutorius.org
+#
+# 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
+
+import os
+import logging
+import copy
+
+logger = logging.getLogger("ResourceTranslator")
+
+from sugar.tutorius.properties import *
+# TODO : Uncomment this line upon integration with the Vault
+##from sugar.tutorius.vault import *
+
+class ResourceTranslator(object):
+ """
+ Handles the conversion of resource properties into file
+ properties before action execution. This class works as a decorator
+ to the ProbeManager class, as it is meant to be a transparent layer
+ before sending the action to execution.
+
+ An architectural note : every different type of translation should have its
+ own method (translate_resource, etc...), and this function must be called
+ from the translate method, under the type test. The translation_* method
+ must take in the input property and give the output property that should
+ replace it.
+ """
+
+ def __init__(self, probe_manager, tutorial_id, testing=False):
+ """
+ Creates a new ResourceTranslator for the given tutorial. This
+ translator is tasked with replacing resource properties of the
+ incoming action into actually usable file properties pointing
+ to the correct resource file. This is done by querying the vault
+ for all the resources and creating a new file property from the
+ returned path.
+
+ @param probe_manager The probe manager to decorate
+ @param tutorial_id The ID of the current tutorial
+
+ @param testing Triggers the usage of a fake vault for testing purposes
+ """
+ self._probe_manager = probe_manager
+ self._tutorial_id = tutorial_id
+
+ self._testing = testing
+
+ def translate_resource(self, res_prop):
+ """
+ Replace the TResourceProperty in the container by their
+ runtime-defined file equivalent. Since the resources are stored
+ in a relative manner in the vault and that only the Vault knows
+ which is the current folder for the current tutorial, it needs
+ to transform the resource identifier into the absolute path for
+ the process to be able to use it properly.
+
+ @param res_prop The resource property to be translated
+ @return The TFileProperty corresponding to this resource, containing
+ an absolute path to the resource
+ """
+ # We need to replace the resource by a file representation
+ filepath = ""
+ # TODO : Refactor when the Vault will be available
+ if not self._testing:
+ filepath = Vault.get_resource_path(self._tutorial_id, \
+ prop_object.value)
+ else:
+ filepath = "/tmp/tutorius/temp.txt"
+ # Create the new file representation
+ file_prop = TFileProperty(filepath)
+
+ return file_prop
+
+ def translate(self, prop_container):
+ """
+ Applies the required translation to be able to send the container to an
+ executing endpoint (like a Probe). For each type of property that
+ requires it, there is translation function that will take care of
+ mapping the property to its executable form.
+
+ This function does not return anything, but its post-condition is
+ that all the properties of the input container have been replaced
+ by their corresponding executable versions.
+
+ An example of translation is taking a resource (a relative path to
+ a file under the tutorial folder) and transforming it into a file
+ (a full absolute path) to be able to load it when the activity receives
+ the action.
+
+ @param prop_container The property container in which we want to
+ replace all the resources for file properties and
+ to recursively do so for addon and addonlist
+ properties.
+ """
+ for propname in prop_container.get_properties():
+ prop_value = getattr(prop_container, propname)
+ prop_type = getattr(type(prop_container), propname).type
+
+ # If the property is a resource, then we need to query the
+ # vault to create its correspondent
+ if prop_type == "resource":
+ # Apply the translation
+ file_prop = self.translate_resource(prop_value)
+ # Set the property with the new value
+ setattr(prop_container, propname, file_prop)
+
+ # If the property is an addon, then its value IS a
+ # container too - we need to translate it
+ elif prop_type == "addon":
+ # Translate the sub properties
+ self.translate(prop_value)
+
+ # If the property is an addon list, then we need to translate all
+ # the elements of the list
+ elif prop_type == "addonlist":
+ # Now, for each sub-container in the list, we will apply the
+ # translation processing. This is done by popping the head of
+ # the list, translating it and inserting it back at the end.
+ for index in range(0, len(prop_value)):
+ # Pop the head of the list
+ container = prop_value[0]
+ del prop_value[0]
+ # Translate the sub-container
+ self.translate(container)
+
+ # Put the value back in the list
+ prop_value.append(container)
+ # Change the list contained in the addonlist property, since
+ # we got a copy of the list when requesting it
+ setattr(prop_container, propname, prop_value)
+
+ ### ProbeManager interface for decorator ###
+
+ ## Unchanged functions ##
+ def setCurrentActivity(self, activity_id):
+ self._probe_manager.setCurrentActivity(activity_id)
+
+ def getCurrentActivity(self):
+ return self._probe_manager.getCurrentActivity()
+
+ currentActivity = property(fget=getCurrentActivity, fset=setCurrentActivity)
+ def attach(self, activity_id):
+ self._probe_manager.attach(activity_id)
+
+ def detach(self, activity_id):
+ self._probe_manager.detach(activity_id)
+
+ def subscribe(self, event, callback):
+ return self._probe_manager.subscribe(event, callback)
+
+ def unsubscribe(self, event, callback):
+ return self._probe_manager.unsubscribe(event, callback)
+
+ def unsubscribe_all(self):
+ return self._probe_manager.unsubscribe_all()
+
+ ## Decorated functions ##
+ def install(self, action):
+ # 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
+ new_action = copy.deepcopy(action)
+ # Execute the replacement
+ self.translate(new_action)
+
+ # Send the new action to the probe manager
+ return self._probe_manager.install(new_action)
+
+ def update(self, action):
+ new_action = copy.deepcopy(action)
+ self.translate(new_action)
+
+ return self._probe_manager.update(new_action)
+
+ def uninstall(self, action):
+ new_action = copy.deepcopy(action)
+ self.translate(new_action)
+
+ return self._probe_manager.uninstall(new_action)
+
+ def uninstall_all(self):
+ return self._probe_manager.uninstall_all()
+
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 ec08d01..22fa940 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,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__"
RESSOURCES_FOLDER = 'ressources'
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 tutorail 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
@@ -243,7 +255,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)
@@ -254,15 +267,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 +294,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")
@@ -304,7 +324,7 @@ class Vault(object):
@staticmethod
- def deleteTutorial(Guid):
+ def deleteTutorial(Tutorial):
"""
Removes the tutorial from the Vault. It will unpublish the tutorial if need be,
and it will also wipe it from the persistent storage.
@@ -396,7 +416,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 +426,7 @@ class Serializer(object):
"""
raise NotImplementedError()
- def load_fsm(self):
+ def load_tutorial(self):
"""
Load fsm from disk.
"""
@@ -417,21 +437,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 +483,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 +517,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 +542,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 +642,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 +708,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 +726,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 +755,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 +764,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
@@ -919,7 +978,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):