From cf40c1951f4f0f26090226fb4969ca147341a031 Mon Sep 17 00:00:00 2001 From: mike Date: Fri, 02 Oct 2009 22:24:56 +0000 Subject: LP 439980 : Refactored the XMLSerializer to support complex components; Corrected specs for the addons properties and constructor parameters names; Moved all existing actions and events to components (except a few left in code for testing purposes) --- (limited to 'tutorius') diff --git a/tutorius/actions.py b/tutorius/actions.py index 4269cd7..0db7988 100644 --- a/tutorius/actions.py +++ b/tutorius/actions.py @@ -176,149 +176,149 @@ class Action(TPropContainer): x, y = self._drag.position self.position = [int(x), int(y)] self.__edit_img.destroy() - -class OnceWrapper(Action): - """ - Wraps a class to perform an action once only - - This ConcreteActions's do() method will only be called on the first do() - and the undo() will be callable after do() has been called - """ - - _action = TAddonProperty() - - def __init__(self, action): - Action.__init__(self) - self._called = False - self._need_undo = False - self._action = action - - def do(self): - """ - Do the action only on the first time - """ - if not self._called: - self._called = True - self._action.do() - self._need_undo = True - - def undo(self): - """ - Undo the action if it's been done - """ - if self._need_undo: - self._action.undo() - self._need_undo = False - -class WidgetIdentifyAction(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 = WidgetIdentifier(self.activity) - self._dialog.show() - - - def undo(self): - if self._dialog: - self._dialog.destroy() - -class ChainAction(Action): - """Utility class to allow executing actions in a specific order""" - def __init__(self, *actions): - """ChainAction(action1, ... ) builds a chain of actions""" - Action.__init__(self) - self._actions = actions - - def do(self,**kwargs): - """do() each action in the chain""" - for act in self._actions: - act.do(**kwargs) - - def undo(self): - """undo() each action in the chain, starting with the last""" - for act in reversed(self._actions): - act.undo() - -class DisableWidgetAction(Action): - def __init__(self, target): - """Constructor - @param target target treeish - """ - Action.__init__(self) - self._target = target - self._widget = None - - def do(self): - """Action do""" - os = ObjectStore() - if os.activity: - self._widget = gtkutils.find_widget(os.activity, self._target) - if self._widget: - self._widget.set_sensitive(False) - - def undo(self): - """Action undo""" - if self._widget: - self._widget.set_sensitive(True) - - -class TypeTextAction(Action): - """ - Simulate a user typing text in a widget - Work on any widget that implements a insert_text method - - @param widget The treehish representation of the widget - @param text the text that is typed - """ - def __init__(self, widget, text): - Action.__init__(self) - - self._widget = widget - self._text = text - - def do(self, **kwargs): - """ - Type the text - """ - widget = gtkutils.find_widget(ObjectStore().activity, self._widget) - if hasattr(widget, "insert_text"): - widget.insert_text(self._text, -1) - - def undo(self): - """ - no undo - """ - pass + +##class OnceWrapper(Action): +## """ +## Wraps a class to perform an action once only +## +## This ConcreteActions's do() method will only be called on the first do() +## and the undo() will be callable after do() has been called +## """ +## +## _action = TAddonProperty() +## +## def __init__(self, action): +## Action.__init__(self) +## self._called = False +## self._need_undo = False +## self._action = action +## +## def do(self): +## """ +## Do the action only on the first time +## """ +## if not self._called: +## self._called = True +## self._action.do() +## self._need_undo = True +## +## def undo(self): +## """ +## Undo the action if it's been done +## """ +## if self._need_undo: +## self._action.undo() +## self._need_undo = False +## +##class WidgetIdentifyAction(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 = WidgetIdentifier(self.activity) +## self._dialog.show() + + +## def undo(self): +## if self._dialog: +## self._dialog.destroy() + +##class ChainAction(Action): +## """Utility class to allow executing actions in a specific order""" +## def __init__(self, *actions): +## """ChainAction(action1, ... ) builds a chain of actions""" +## Action.__init__(self) +## self._actions = actions +## +## def do(self,**kwargs): +## """do() each action in the chain""" +## for act in self._actions: +## act.do(**kwargs) +## +## def undo(self): +## """undo() each action in the chain, starting with the last""" +## for act in reversed(self._actions): +## act.undo() + +##class DisableWidgetAction(Action): +## def __init__(self, target): +## """Constructor +## @param target target treeish +## """ +## Action.__init__(self) +## self._target = target +## self._widget = None + +## def do(self): +## """Action do""" +## os = ObjectStore() +## if os.activity: +## self._widget = gtkutils.find_widget(os.activity, self._target) +## if self._widget: +## self._widget.set_sensitive(False) -class ClickAction(Action): - """ - Action that simulate a click on a widget - Work on any widget that implements a clicked() method +## def undo(self): +## """Action undo""" +## if self._widget: +## self._widget.set_sensitive(True) - @param widget The threehish representation of the widget - """ - def __init__(self, widget): - Action.__init__(self) - self._widget = widget - def do(self): - """ - click the widget - """ - widget = gtkutils.find_widget(ObjectStore().activity, self._widget) - if hasattr(widget, "clicked"): - widget.clicked() - - def undo(self): - """ - No undo - """ - pass +##class TypeTextAction(Action): +## """ +## Simulate a user typing text in a widget +## Work on any widget that implements a insert_text method +## +## @param widget The treehish representation of the widget +## @param text the text that is typed +## """ +## def __init__(self, widget, text): +## Action.__init__(self) +## +## self._widget = widget +## self._text = text +## +## def do(self, **kwargs): +## """ +## Type the text +## """ +## widget = gtkutils.find_widget(ObjectStore().activity, self._widget) +## if hasattr(widget, "insert_text"): +## widget.insert_text(self._text, -1) +## +## def undo(self): +## """ +## no undo +## """ +## pass +## +##class ClickAction(Action): +## """ +## Action that simulate a click on a widget +## Work on any widget that implements a clicked() method +## +## @param widget The threehish representation of the widget +## """ +## def __init__(self, widget): +## Action.__init__(self) +## self._widget = widget +## +## def do(self): +## """ +## click the widget +## """ +## widget = gtkutils.find_widget(ObjectStore().activity, self._widget) +## if hasattr(widget, "clicked"): +## widget.clicked() +## +## def undo(self): +## """ +## No undo +## """ +## pass diff --git a/tutorius/addon.py b/tutorius/addon.py index 51791d1..e311a65 100644 --- a/tutorius/addon.py +++ b/tutorius/addon.py @@ -56,6 +56,12 @@ def create(name, *args, **kwargs): if not _cache: _reload_addons() try: + comp_metadata = _cache[name] + try: + return comp_metadata['class'](*args, **kwargs) + except: + logging.error("Could not instantiate %s with parameters %s, %s"%(comp_metadata['name'],str(args), str(kwargs))) + return None return _cache[name]['class'](*args, **kwargs) except KeyError: logging.error("Addon not found for class '%s'", name) diff --git a/tutorius/bundler.py b/tutorius/bundler.py index 8808d93..734c679 100644 --- a/tutorius/bundler.py +++ b/tutorius/bundler.py @@ -48,6 +48,46 @@ INI_XML_FSM_PROPERTY = "FSM_FILENAME" INI_FILENAME = "meta.ini" TUTORIAL_FILENAME = "tutorial.xml" NODE_COMPONENT = "Component" +NODE_SUBCOMPONENT = "SubComponent" +NODE_SUBCOMPONENTLIST = "SubComponentList" + +class Vault(object): + """ + The Vault is the primary interface for the storage and installation of tutorials + on the machine. It needs to accomplish the following tasks : + - query() : Lists the + - installTutorial() : + - deleteTutorial() : + - readTutorial() : + - saveTutorial() : + """ + def query(keyword="", category="", start_index=0, num_results=10): + """ + Returns a list of tutorial meta-data corresponding to the keywords + and category mentionned. + + @param keyword The keyword to look for in the tutorial title and description. + @param category The category in which to look for tutorials + @param start_index The first result to be shown (e.g. ) + @param num_results The number of results to show + @return The list of tutorial metadata that corresponds to the query parameters. + """ + raise NotImplementedError("The query function on the Vault is not implemented") + + def installTutorial(path ,force_install=False): + """ + Inserts the tutorial inside the Vault. Once installed, it will show up + """ + raise NotImplementedError("Installation in the Vault not supported yet") + + def deleteTutorial(tutorial_id): + raise NotImplementedError("") + + def readTutorial(tutorial_id): + raise NotImplementedError("") + + def saveTutorial(tutorial, metadata, resource_list): + raise NotImplementedError("") class TutorialStore(object): @@ -150,6 +190,71 @@ class XMLSerializer(Serializer): eventfiltersList = stateNode.appendChild(self._create_event_filters_node(state.get_event_filter_list(), doc)) return statesList + def _create_addon_component_node(self, 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. + + e.g. + + + + + + + When reloading this node, we should look up the property name for the parent + in the attribute of the node, then examine the subnode to create the addon + object itself. + + @param parent_attr_name The name of the parent's attribute for this addon + e.g. the OnceWrapper has the action attribute, which corresponds to a + sub-action it must execute once. + @param comp The component node itself + @param doc The XML document root (only used to create the nodes) + @returns A NODE_SUBCOMPONENT node, with the property attribute and a sub node + that represents another component. + """ + subCompNode = doc.createElement(NODE_SUBCOMPONENT) + subCompNode.setAttribute("property", parent_attr_name) + + subNode = self._create_component_node(comp, doc) + + subCompNode.appendChild(subNode) + + return subCompNode + + def _create_addonlist_component_node(self, 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 + name. + + e.g. + + + + + + + + When reloading this node, we should look up the property name for the parent + in the the attribute of the node, then rebuild the list by appending the + content of all the subnodes. + + @param parent_attr_name The name of the parent component's property + @param comp_list A list of components that comprise the property + @param doc The XML document root (only for creating new nodes) + @returns A NODE_SUBCOMPONENTLIST node with the property attribute + """ + subCompListNode = doc.createElement(NODE_SUBCOMPONENTLIST) + subCompListNode.setAttribute("property", parent_attr_name) + + for comp in comp_list: + compNode = self._create_component_node(comp, doc) + subCompListNode.appendChild(compNode) + + return subCompListNode + def _create_component_node(self, comp, doc): """ Takes a single component (action or eventfilter) and transforms it @@ -169,10 +274,10 @@ class XMLSerializer(Serializer): for propname in comp.get_properties(): propval = getattr(comp, propname) if getattr(type(comp), propname).type == "addonlist": - for subval in propval: - compNode.appendChild(self._create_component_node(subval, doc)) - elif getattr(type(comp), propname).type == "addonlist": - compNode.appendChild(self._create_component_node(subval, doc)) + compNode.appendChild(self._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)) else: # repr instead of str, as we want to be able to eval() it into a # valid object. @@ -282,6 +387,27 @@ class XMLSerializer(Serializer): # Error : none of these directories contain the tutorial raise IOError(2, "Neither the global nor the bundle directory contained the tutorial with GUID %s"%guid) + def _get_direct_descendants_by_tag_name(self, node, name): + """ + Searches in the list of direct descendants of a node to find all the node + that have the given name. + + This is used because the Document.getElementsByTagName() function returns the + list of all the descendants (whatever their distance to the start node) that + have that name. In the case of complex components, we absolutely need to inspect + a single layer of the tree at the time. + + @param node The node from which we want the direct descendants with a particular + name + @param name The name of the node + @returns A list, possibly empty, of direct descendants of node that have this name + """ + return_list = [] + for childNode in node.childNodes: + if childNode.nodeName == name: + return_list.append(childNode) + return return_list + def _load_xml_properties(self, properties_elem): """ Changes a list of properties into fully instanciated properties. @@ -298,7 +424,7 @@ class XMLSerializer(Serializer): @param filters_elem An XML Element representing a list of event filters """ reformed_event_filters_list = [] - event_filter_element_list = filters_elem.getElementsByTagName(NODE_COMPONENT) + event_filter_element_list = self._get_direct_descendants_by_tag_name(filters_elem, NODE_COMPONENT) new_event_filter = None for event_filter in event_filter_element_list: @@ -309,6 +435,42 @@ class XMLSerializer(Serializer): return reformed_event_filters_list + def _load_xml_subcomponents(self, node, properties): + """ + Loads all the subcomponent node below the given node and inserts them with + the right property name inside the properties dictionnary. + + @param node The parent node that contains one or many SubComponent nodes. + @param properties A dictionnary where the subcomponent property names + 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) + + for subComp in subCompList: + property_name = subComp.getAttribute("property") + internal_comp_node = self._get_direct_descendants_by_tag_name(subComp, NODE_COMPONENT)[0] + internal_comp = self._load_xml_component(internal_comp_node) + properties[str(property_name)] = internal_comp + + def _load_xml_subcomponent_lists(self, node, properties): + """ + Loads all the subcomponent lists below the given node and stores them + under the correct property name for that node. + + @param node The node from which we want to read the subComponent lists + @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) + for subCompListNode in listOf_subCompListNode: + property_name = subCompListNode.getAttribute("property") + subCompList = [] + for subCompNode in self._get_direct_descendants_by_tag_name(subCompListNode, NODE_COMPONENT): + subComp = self._load_xml_component(subCompNode) + subCompList.append(subComp) + properties[str(property_name)] = subCompList + def _load_xml_component(self, node): """ Loads a single addon component instance from an Xml node. @@ -318,20 +480,23 @@ class XMLSerializer(Serializer): @return The addon component object of the correct type according to the XML description """ - new_action = addon.create(node.getAttribute("Class")) - if not new_action: - return None + class_name = node.getAttribute("Class") + + properties = {} + + for prop in node.attributes.keys(): + if prop == "Class" : continue + # security : keep sandboxed + properties[str(prop)] = eval(node.getAttribute(prop)) + + # Read the complex attributes + self._load_xml_subcomponents(node, properties) + self._load_xml_subcomponent_lists(node, properties) - for attrib in node.attributes.keys(): - if attrib == "Class": continue - # security note: keep sandboxed - setattr(new_action, attrib, eval(node.getAttribute(attrib), {}, {})) + new_action = addon.create(class_name, **properties) - # recreate complex attributes - for sub in node.childNodes: - name = getattr(new_action, sub.nodeName) - if name == "addon": - setattr(new_action, sub.getAttribute("Name"), self._load_xml_action(sub)) + if not new_action: + return None return new_action @@ -342,7 +507,7 @@ class XMLSerializer(Serializer): @param actions_elem An XML Element representing a list of Actions """ reformed_actions_list = [] - actions_element_list = actions_elem.getElementsByTagName(NODE_COMPONENT) + actions_element_list = self._get_direct_descendants_by_tag_name(actions_elem, NODE_COMPONENT) for action in actions_element_list: new_action = self._load_xml_component(action) diff --git a/tutorius/core.py b/tutorius/core.py index dd2435e..41089f1 100644 --- a/tutorius/core.py +++ b/tutorius/core.py @@ -258,6 +258,42 @@ class State(object): tutorial. """ self._event_filters = [] + + def is_identical(self, otherState): + """ + Compares two states and tells whether they contain the same states and + +` """ + if not isinstance(otherState, State): + return False + if self.name != otherState.name: + return False + + # Do they have the same actions? + if len(self._actions) != len(otherState._actions): + return False + for act in self._actions: + found = False + for otherAct in otherState._actions: + if act.is_identical(otherAct): + found = True + break + if found == False: + return False + + # Do they have the same event filters? + if len(self._actions) != len(otherState._actions): + return False + for event in self._event_filters: + found = False + for otherEvent in otherState._event_filters: + if event.is_identical(otherEvent): + found = True + break + if found == False: + return False + + return True class FiniteStateMachine(State): """ @@ -526,3 +562,43 @@ class FiniteStateMachine(State): for st in self._states.itervalues(): out_string += st.name + ", " return out_string + + def is_identical(self, otherFSM): + """ + Compares the elements of two FSM to ensure and returns true if they have the + same set of states, containing the same actions and the same event filters. + + @returns True if the two FSMs have the same content false otherwise + """ + if not isinstance(otherFSM, FiniteStateMachine): + return False + + if not (self.name == otherFSM.name) or \ + not (self.start_state_name == otherFSM.start_state_name): + return False + + if len(self._actions) != len(otherFSM._actions): + return False + # Test that we have all the same FSM level actions + for act in self._actions: + found = False + for otherAct in otherFSM._actions: + if act.is_identical(otherAct): + found = True + break + if found == False: + return False + + if len(self._states) != len(otherFSM._states): + return False + + for state in self._states.itervalues(): + found = False + for otherState in otherFSM._states.itervalues(): + if state.is_identical(otherState): + found = True + break + if found == False: + return False + + return True diff --git a/tutorius/filters.py b/tutorius/filters.py index aa8c997..fc58562 100644 --- a/tutorius/filters.py +++ b/tutorius/filters.py @@ -94,111 +94,111 @@ class EventFilter(properties.TPropContainer): if self._callback: self._callback(self) -class TimerEvent(EventFilter): - """ - TimerEvent is a special EventFilter that uses gobject - timeouts to trigger a state change after a specified amount - of time. It must be used inside a gobject main loop to work. - """ - def __init__(self,next_state,timeout_s): - """Constructor. - - @param next_state default EventFilter param, passed on to EventFilter - @param timeout_s timeout in seconds - """ - super(TimerEvent,self).__init__(next_state) - self._timeout = timeout_s - self._handler_id = None - - def install_handlers(self, callback, **kwargs): - """install_handlers creates the timer and starts it""" - super(TimerEvent,self).install_handlers(callback, **kwargs) - #Create the timer - self._handler_id = gobject.timeout_add_seconds(self._timeout, self._timeout_cb) - - def remove_handlers(self): - """remove handler removes the timer""" - super(TimerEvent,self).remove_handlers() - if self._handler_id: - try: - #XXX What happens if this was already triggered? - #remove the timer - gobject.source_remove(self._handler_id) - except: - pass - - def _timeout_cb(self): - """ - _timeout_cb triggers the eventfilter callback. - - It is necessary because gobject timers only stop if the callback they - trigger returns False - """ - self.do_callback() - return False #Stops timeout - -class GtkWidgetTypeFilter(EventFilter): - """ - Event Filter that listens for keystrokes on a widget - """ - def __init__(self, next_state, object_id, text=None, strokes=None): - """Constructor - @param next_state default EventFilter param, passed on to EventFilter - @param object_id object tree-ish identifier - @param text resulting text expected - @param strokes list of strokes expected - - At least one of text or strokes must be supplied - """ - super(GtkWidgetTypeFilter, self).__init__(next_state) - self._object_id = object_id - self._text = text - self._captext = "" - self._strokes = strokes - self._capstrokes = [] - self._widget = None - self._handler_id = None - - def install_handlers(self, callback, **kwargs): - """install handlers - @param callback default EventFilter callback arg - """ - super(GtkWidgetTypeFilter, self).install_handlers(callback, **kwargs) - logger.debug("~~~GtkWidgetTypeFilter install") - activity = ObjectStore().activity - if activity is None: - logger.error("No activity") - raise RuntimeWarning("no activity in the objectstore") - - self._widget = find_widget(activity, self._object_id) - if self._widget: - self._handler_id= self._widget.connect("key-press-event",self.__keypress_cb) - logger.debug("~~~Connected handler %d on %s" % (self._handler_id,self._object_id) ) - - def remove_handlers(self): - """remove handlers""" - super(GtkWidgetTypeFilter, self).remove_handlers() - #if an event was connected, disconnect it - if self._handler_id: - self._widget.handler_disconnect(self._handler_id) - self._handler_id=None - - def __keypress_cb(self, widget, event, *args): - """keypress callback""" - logger.debug("~~~keypressed!") - key = event.keyval - keystr = event.string - logger.debug("~~~Got key: " + str(key) + ":"+ keystr) - self._capstrokes += [key] - #TODO Treat other stuff, such as arrows - if key == gtk.keysyms.BackSpace: - self._captext = self._captext[:-1] - else: - self._captext = self._captext + keystr - - logger.debug("~~~Current state: " + str(self._capstrokes) + ":" + str(self._captext)) - if not self._strokes is None and self._strokes in self._capstrokes: - self.do_callback() - if not self._text is None and self._text in self._captext: - self.do_callback() +##class TimerEvent(EventFilter): +## """ +## TimerEvent is a special EventFilter that uses gobject +## timeouts to trigger a state change after a specified amount +## of time. It must be used inside a gobject main loop to work. +## """ +## def __init__(self,next_state,timeout_s): +## """Constructor. +## +## @param next_state default EventFilter param, passed on to EventFilter +## @param timeout_s timeout in seconds +## """ +## super(TimerEvent,self).__init__(next_state) +## self._timeout = timeout_s +## self._handler_id = None +## +## def install_handlers(self, callback, **kwargs): +## """install_handlers creates the timer and starts it""" +## super(TimerEvent,self).install_handlers(callback, **kwargs) +## #Create the timer +## self._handler_id = gobject.timeout_add_seconds(self._timeout, self._timeout_cb) +## +## def remove_handlers(self): +## """remove handler removes the timer""" +## super(TimerEvent,self).remove_handlers() +## if self._handler_id: +## try: +## #XXX What happens if this was already triggered? +## #remove the timer +## gobject.source_remove(self._handler_id) +## except: +## pass +## +## def _timeout_cb(self): +## """ +## _timeout_cb triggers the eventfilter callback. +## +## It is necessary because gobject timers only stop if the callback they +## trigger returns False +## """ +## self.do_callback() +## return False #Stops timeout +## +##class GtkWidgetTypeFilter(EventFilter): +## """ +## Event Filter that listens for keystrokes on a widget +## """ +## def __init__(self, next_state, object_id, text=None, strokes=None): +## """Constructor +## @param next_state default EventFilter param, passed on to EventFilter +## @param object_id object tree-ish identifier +## @param text resulting text expected +## @param strokes list of strokes expected +## +## At least one of text or strokes must be supplied +## """ +## super(GtkWidgetTypeFilter, self).__init__(next_state) +## self._object_id = object_id +## self._text = text +## self._captext = "" +## self._strokes = strokes +## self._capstrokes = [] +## self._widget = None +## self._handler_id = None +## +## def install_handlers(self, callback, **kwargs): +## """install handlers +## @param callback default EventFilter callback arg +## """ +## super(GtkWidgetTypeFilter, self).install_handlers(callback, **kwargs) +## logger.debug("~~~GtkWidgetTypeFilter install") +## activity = ObjectStore().activity +## if activity is None: +## logger.error("No activity") +## raise RuntimeWarning("no activity in the objectstore") +## +## self._widget = find_widget(activity, self._object_id) +## if self._widget: +## self._handler_id= self._widget.connect("key-press-event",self.__keypress_cb) +## logger.debug("~~~Connected handler %d on %s" % (self._handler_id,self._object_id) ) +## +## def remove_handlers(self): +## """remove handlers""" +## super(GtkWidgetTypeFilter, self).remove_handlers() +## #if an event was connected, disconnect it +## if self._handler_id: +## self._widget.handler_disconnect(self._handler_id) +## self._handler_id=None +## +## def __keypress_cb(self, widget, event, *args): +## """keypress callback""" +## logger.debug("~~~keypressed!") +## key = event.keyval +## keystr = event.string +## logger.debug("~~~Got key: " + str(key) + ":"+ keystr) +## self._capstrokes += [key] +## #TODO Treat other stuff, such as arrows +## if key == gtk.keysyms.BackSpace: +## self._captext = self._captext[:-1] +## else: +## self._captext = self._captext + keystr +## +## logger.debug("~~~Current state: " + str(self._capstrokes) + ":" + str(self._captext)) +## if not self._strokes is None and self._strokes in self._capstrokes: +## self.do_callback() +## if not self._text is None and self._text in self._captext: +## self.do_callback() diff --git a/tutorius/properties.py b/tutorius/properties.py index abf76e5..6d30a8d 100644 --- a/tutorius/properties.py +++ b/tutorius/properties.py @@ -95,6 +95,39 @@ class TPropContainer(object): """ return object.__getattribute__(self, "_props").keys() + def is_identical(self, otherContainer): + for prop in self._props.keys(): + found = False + for otherProp in otherContainer._props.keys(): + if prop == otherProp: + this_type = getattr(type(self), prop).type + other_type = getattr(type(otherContainer), prop).type + if this_type != other_type: + return False + if this_type == "addonlist": + for inner_cont in self._props[prop]: + inner_found = False + for other_inner in otherContainer._props[prop]: + if inner_cont.is_identical(other_inner): + inner_found = True + break + if inner_found == False: + return False + found = True + break + elif this_type == "addon": + if not self._props[prop].is_identical(otherContainer._props[prop]): + return False + found = True + break + else: + if self._props[prop]== otherContainer._props[prop]: + found = True + break + if found == False: + return False + return True + class TutoriusProperty(object): """ The base class for all actions' properties. The interface is the following : @@ -317,8 +350,15 @@ class TAddonListProperty(TutoriusProperty): See TAddonProperty """ def __init__(self): - super(TAddonProperty, self).__init__() + TutoriusProperty.__init__(self) self.type = "addonlist" self.default = [] + def validate(self, value): + if isinstance(value, list): + for component in value: + if not (isinstance(component, TPropContainer)): + raise ValueError("Expected a list of TPropContainer instances inside TAddonListProperty value, got a %s" % (str(type(component)))) + return value + raise ValueError("Value proposed to TAddonListProperty is not a list") diff --git a/tutorius/uam/__init__.py b/tutorius/uam/__init__.py index 7cf5671..bcd67e1 100644 --- a/tutorius/uam/__init__.py +++ b/tutorius/uam/__init__.py @@ -65,7 +65,8 @@ for subscheme in [".".join([SCHEME,s]) for s in __parsers]: class SchemeError(Exception): def __init__(self, message): Exception.__init__(self, message) - self.message = message + ## Commenting this line as it is causing an error in the tests + ##self.message = message def parse_uri(uri): -- cgit v0.9.1