# HG changeset patch # User Goffi # Date 1552237394 -3600 # Node ID e2595c81eb6d1faff948e56893f937cd7f849866 # Parent 7764383a968c1a3e2d32af85f13b9dcac470eeb3 quick frontend(widgets): improved handling of multiple instances of widgets: when a widget was recreated, a new hash was created with a suffix, making hash matching difficult and bug prone. This has been changed to use a list of instances instead, this simplify the code and avoid issues while looking for existing instances of a widget. diff -r 7764383a968c -r e2595c81eb6d sat_frontends/quick_frontend/quick_widgets.py --- a/sat_frontends/quick_frontend/quick_widgets.py Sun Mar 10 18:03:11 2019 +0100 +++ b/sat_frontends/quick_frontend/quick_widgets.py Sun Mar 10 18:03:14 2019 +0100 @@ -24,7 +24,6 @@ from sat_frontends.quick_frontend.constants import Const as C -NEW_INSTANCE_SUFF = "_new_instance_" classes_map = {} @@ -65,8 +64,9 @@ def __iter__(self): """Iterate throught all widgets""" for widget_map in self._widgets.itervalues(): - for widget in widget_map.itervalues(): - yield widget + for widget_instances in widget_map.itervalues(): + for widget in widget_instances: + yield widget def getRealClass(self, class_): """Return class registered for given class_ @@ -86,21 +86,29 @@ ) return cls - def getRootHash(self, hash_): - """Return root hash (i.e. hash without new instance suffix for recreated widgets + def getWidgetInstances(self, widget): + """Get all instance of a widget - @param hash_(immutable): hash of a widget - @return (unicode): root hash (transtyped to unicode) + This is a helper method which call getWidgets + @param widget(QuickWidget): retrieve instances of this widget + @return: iterator on widgets """ - return unicode(hash_).split(NEW_INSTANCE_SUFF)[0] + try: + target = widget.target + except AttributeError: + target = next(iter(widget.targets)) + return self.getWidgets(widget.__class__, target, widget.profiles) def getWidgets(self, class_, target=None, profiles=None): """Get all subclassed widgets instances - @param class_: subclass of QuickWidget, same parameter as used in [getOrCreateWidget] - @param target: if not None, construct a hash with this target and filter corresponding widgets - recreated widgets (with new instance suffix) are handled - @param profiles(iterable, None): if not None, filter on instances linked to these profiles + @param class_: subclass of QuickWidget, same parameter as used in + [getOrCreateWidget] + @param target: if not None, construct a hash with this target and filter + corresponding widgets + recreated widgets are handled + @param profiles(iterable, None): if not None, filter on instances linked to these + profiles @return: iterator on widgets """ class_ = self.getRealClass(class_) @@ -113,18 +121,18 @@ filter_hash = unicode(class_.getWidgetHash(target, profiles)) else: filter_hash = None - for w_hash, w in widgets_map.iteritems(): - if profiles is None or w.profiles.intersection(profiles): - if ( - filter_hash is not None - and self.getRootHash(w_hash) != filter_hash - ): - continue - yield w + if filter_hash is not None: + for widget in widgets_map[filter_hash]: + yield widget + else: + for widget_instances in widgets_map.itervalues(): + for widget in widget_instances: + yield widget def getWidget(self, class_, target=None, profiles=None): """Get a widget without creating it if it doesn't exist. + if several instances of widgets with this hash exist, the first one is returned @param class_: subclass of QuickWidget, same parameter as used in [getOrCreateWidget] @param target: target depending of the widget, usually a JID instance @param profiles (unicode, iterable[unicode], None): profile(s) to use (may or may not be @@ -137,7 +145,7 @@ class_ = self.getRealClass(class_) hash_ = class_.getWidgetHash(target, profiles) try: - return self._widgets[class_.__name__][hash_] + return self._widgets[class_.__name__][hash_][0] except KeyError: return None @@ -158,10 +166,10 @@ if 'on_existing_widget' is present it can have the following values: C.WIDGET_KEEP [default]: return the existing widget C.WIDGET_RAISE: raise WidgetAlreadyExistsError - C.WIDGET_RECREATE: create a new widget *WITH A NEW HASH* + C.WIDGET_RECREATE: create a new widget if the existing widget has a "recreateArgs" method, it will be called with args list and kwargs dict so the values can be completed to create correctly the new instance - [callable]: this method will be called with existing widget as argument + [callable]: this method will be called with existing widget as argument, the widget to use must be returned if 'force_hash' is present, the hash given in value will be used instead of the one returned by class_.getWidgetHash other keys will be used to instanciate class_ if the case happen (e.g. if type_ is present and class_ is a QuickChat subclass, it will be used to create a new QuickChat instance). @@ -211,16 +219,17 @@ widget = None # if the class is not SINGLE, we always create a new widget else: try: - widget = widgets_map[hash_] - widget.addTarget(target) + widget = widgets_map[hash_][0] except KeyError: widget = None + else: + widget.addTarget(target) if widget is None: # we need to create a new widget log.debug(u"Creating new widget for target {} {}".format(target, cls)) widget = cls(*_args, **_kwargs) - widgets_map[hash_] = widget + widgets_map[hash_] = [widget] if on_new_widget == C.WIDGET_NEW: self.host.newWidget(widget) @@ -235,55 +244,24 @@ elif on_existing_widget == C.WIDGET_RAISE: raise WidgetAlreadyExistsError(hash_) elif on_existing_widget == C.WIDGET_RECREATE: - # we use getOrCreateWidget to recreate the new widget - # /!\ we use args and kwargs and not _args and _kwargs because we need the original args - # we need to get rid of kwargs special options - new_kwargs = kwargs.copy() - try: - new_kwargs.pop( - "force_hash" - ) # FIXME: we use pop instead of del here because pyjamas doesn't raise error on del - except KeyError: - pass - else: - raise ValueError( - "force_hash option can't be used with on_existing_widget=RECREATE" - ) - - new_kwargs["on_new_widget"] = on_new_widget - - # XXX: keep up-to-date if new special kwargs are added (i.e.: delete these keys here) - new_kwargs["on_existing_widget"] = C.WIDGET_RAISE try: recreateArgs = widget.recreateArgs except AttributeError: pass else: - recreateArgs(args, new_kwargs) - hash_idx = 1 - while True: - new_kwargs["force_hash"] = "{}{}{}".format( - hash_, NEW_INSTANCE_SUFF, hash_idx - ) - try: - widget = self.getOrCreateWidget( - class_, target, *args, **new_kwargs - ) - except WidgetAlreadyExistsError: - hash_idx += 1 - else: - log.debug( - u"Widget already exists, a new one has been recreated with hash {}".format( - new_kwargs["force_hash"] - ) - ) - break + recreateArgs(_args, _kwargs) + widget = cls(*_args, **_kwargs) + widgets_map[hash_].append(widget) + log.debug(u"widget <{wid}> already exists, a new one has been recreated" + .format(wid=widget)) elif callable(on_existing_widget): - on_existing_widget(widget) + widget = on_existing_widget(widget) + if widget is None: + raise exceptions.InternalError( + u"on_existing_widget method must return the widget to use") else: raise exceptions.InternalError( - "Unexpected on_existing_widget value ({})".format(on_existing_widget) - ) + "Unexpected on_existing_widget value ({})".format(on_existing_widget)) return widget @@ -306,9 +284,11 @@ for widget_map in self._widgets.itervalues(): to_delete = set() - for hash_, widget in widget_map.iteritems(): - if widget_to_delete is widget: - to_delete.add(hash_) + for hash_, widget_instances in widget_map.iteritems(): + if widget_to_delete in widget_instances: + widget_instances.remove(widget_to_delete) + if not widget_instances: + to_delete.add(hash_) for hash_ in to_delete: del widget_map[hash_]