diff options
Diffstat (limited to 'tutorius')
-rw-r--r-- | tutorius/actions.py | 1 | ||||
-rw-r--r-- | tutorius/constraints.py | 39 | ||||
-rw-r--r-- | tutorius/core.py | 22 | ||||
-rw-r--r-- | tutorius/editor_interpreter.py | 105 | ||||
-rw-r--r-- | tutorius/events.py | 36 | ||||
-rw-r--r-- | tutorius/ipython_view.py | 301 | ||||
-rw-r--r-- | tutorius/properties.py | 38 | ||||
-rw-r--r-- | tutorius/translator.py | 195 | ||||
-rw-r--r-- | tutorius/tutorial.py | 55 | ||||
-rw-r--r-- | tutorius/vault.py | 349 |
10 files changed, 976 insertions, 165 deletions
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): |