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.
This commit is contained in:
Goffi 2019-03-10 18:03:14 +01:00
commit 7cf0180731

View file

@ -24,7 +24,6 @@ from sat.core import exceptions
from sat_frontends.quick_frontend.constants import Const as C
NEW_INSTANCE_SUFF = "_new_instance_"
classes_map = {}
@ -65,8 +64,9 @@ class QuickWidgetsManager(object):
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 @@ class QuickWidgetsManager(object):
)
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 @@ class QuickWidgetsManager(object):
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 QuickWidgetsManager(object):
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 @@ class QuickWidgetsManager(object):
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 @@ class QuickWidgetsManager(object):
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 @@ class QuickWidgetsManager(object):
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 @@ class QuickWidgetsManager(object):
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_]