scipion-pyworkflow 3.10.6__py3-none-any.whl → 3.11.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. pyworkflow/config.py +131 -67
  2. pyworkflow/constants.py +2 -1
  3. pyworkflow/gui/browser.py +39 -5
  4. pyworkflow/gui/dialog.py +2 -0
  5. pyworkflow/gui/form.py +141 -52
  6. pyworkflow/gui/gui.py +8 -8
  7. pyworkflow/gui/project/project.py +6 -7
  8. pyworkflow/gui/project/searchprotocol.py +91 -7
  9. pyworkflow/gui/project/viewdata.py +1 -1
  10. pyworkflow/gui/project/viewprotocols.py +45 -22
  11. pyworkflow/gui/project/viewprotocols_extra.py +9 -6
  12. pyworkflow/gui/widgets.py +2 -2
  13. pyworkflow/mapper/sqlite.py +4 -4
  14. pyworkflow/plugin.py +93 -44
  15. pyworkflow/project/project.py +158 -70
  16. pyworkflow/project/usage.py +165 -0
  17. pyworkflow/protocol/executor.py +30 -18
  18. pyworkflow/protocol/hosts.py +9 -6
  19. pyworkflow/protocol/launch.py +15 -8
  20. pyworkflow/protocol/params.py +59 -19
  21. pyworkflow/protocol/protocol.py +124 -58
  22. pyworkflow/resources/showj/arrowDown.png +0 -0
  23. pyworkflow/resources/showj/arrowUp.png +0 -0
  24. pyworkflow/resources/showj/background_section.png +0 -0
  25. pyworkflow/resources/showj/colRowModeOff.png +0 -0
  26. pyworkflow/resources/showj/colRowModeOn.png +0 -0
  27. pyworkflow/resources/showj/delete.png +0 -0
  28. pyworkflow/resources/showj/doc_icon.png +0 -0
  29. pyworkflow/resources/showj/download_icon.png +0 -0
  30. pyworkflow/resources/showj/enabled_gallery.png +0 -0
  31. pyworkflow/resources/showj/galleryViewOff.png +0 -0
  32. pyworkflow/resources/showj/galleryViewOn.png +0 -0
  33. pyworkflow/resources/showj/goto.png +0 -0
  34. pyworkflow/resources/showj/menu.png +0 -0
  35. pyworkflow/resources/showj/separator.png +0 -0
  36. pyworkflow/resources/showj/tableViewOff.png +0 -0
  37. pyworkflow/resources/showj/tableViewOn.png +0 -0
  38. pyworkflow/resources/showj/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  39. pyworkflow/resources/showj/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  40. pyworkflow/resources/showj/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  41. pyworkflow/resources/showj/volumeOff.png +0 -0
  42. pyworkflow/resources/showj/volumeOn.png +0 -0
  43. pyworkflow/utils/log.py +15 -6
  44. pyworkflow/utils/properties.py +78 -92
  45. pyworkflow/utils/utils.py +3 -2
  46. pyworkflow/viewer.py +23 -1
  47. pyworkflow/webservices/config.py +0 -3
  48. pyworkflow/webservices/notifier.py +24 -34
  49. pyworkflowtests/protocols.py +1 -3
  50. pyworkflowtests/tests/test_protocol_execution.py +4 -0
  51. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/METADATA +13 -27
  52. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/RECORD +56 -35
  53. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/WHEEL +1 -1
  54. scipion_pyworkflow-3.10.6.dist-info/dependency_links.txt +0 -1
  55. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/entry_points.txt +0 -0
  56. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/licenses/LICENSE.txt +0 -0
  57. {scipion_pyworkflow-3.10.6.dist-info → scipion_pyworkflow-3.11.1.dist-info}/top_level.txt +0 -0
@@ -25,11 +25,11 @@
25
25
  import logging
26
26
  logger = logging.getLogger(__name__)
27
27
 
28
- from pyworkflow import Config, DEFAULT_EXECUTION_ACTION_ASK, DEFAULT_EXECUTION_ACTION_SINGLE
28
+ from pyworkflow import Config, DEFAULT_EXECUTION_ACTION_ASK, DEFAULT_EXECUTION_ACTION_SINGLE, DOCSITEURLS
29
29
  from pyworkflow.gui import LIST_TREEVIEW, \
30
- ShortCut, ToolTip, RESULT_RUN_ALL, RESULT_RUN_SINGLE, RESULT_CANCEL, BORDERLESS_TREEVIEW
30
+ ShortCut, ToolTip, RESULT_RUN_ALL, RESULT_RUN_SINGLE, RESULT_CANCEL, BORDERLESS_TREEVIEW, showInfo
31
31
  from pyworkflow.gui.project.constants import *
32
- from pyworkflow.protocol import SIZE_1MB, SIZE_1GB, SIZE_1TB
32
+ from pyworkflow.protocol import SIZE_1MB, SIZE_1GB, SIZE_1TB, Protocol
33
33
 
34
34
  INIT_REFRESH_SECONDS = Config.SCIPION_GUI_REFRESH_INITIAL_WAIT
35
35
 
@@ -299,7 +299,7 @@ class ProtocolsView(tk.Frame):
299
299
  """ Call appropriate viewer for objId. """
300
300
  proj = self.project
301
301
  obj = proj.getObject(int(objId))
302
- viewerClasses = self.domain.findViewers(obj.getClassName(), DESKTOP_TKINTER)
302
+ viewerClasses = self.domain.findViewers(obj, DESKTOP_TKINTER)
303
303
  if not viewerClasses:
304
304
  return # TODO: protest nicely
305
305
  viewer = viewerClasses[0](project=proj, parent=self.window)
@@ -401,12 +401,12 @@ class ProtocolsView(tk.Frame):
401
401
  def _findProtocol(self, event=None):
402
402
  """ Find a desired protocol by typing some keyword. """
403
403
 
404
- if event is not None and event.widget.widgetName=="canvas":
404
+ if event is not None and self._noSelection() and event.widget.widgetName=="canvas" and self:
405
405
  position = self.runsGraphCanvas.getCoordinates(event)
406
406
  else:
407
407
  position = None
408
408
 
409
- window = SearchProtocolWindow(self.window, position=position)
409
+ window = SearchProtocolWindow(self.window, position=position, selectionGetter=self.getSelectedProtocol)
410
410
  window.show()
411
411
 
412
412
  def _locateProtocol(self, e=None):
@@ -1035,16 +1035,34 @@ class ProtocolsView(tk.Frame):
1035
1035
  self.viewButtons[ACTION_TREE].grid(row=0, column=1)
1036
1036
 
1037
1037
  def _protocolItemClick(self, e=None, position=None):
1038
- """ Callback for the window to add a new protocol."""
1038
+ """ Callback for the window to add a new protocol.
1039
+ """
1039
1040
 
1040
1041
  # Get the tree widget that originated the event
1041
1042
  # it could be the left panel protocols tree or just
1042
- # the search protocol dialog tree
1043
+ # the search protocol dialog tree. In this case now non installed protocols are listed from the suggestions
1043
1044
  tree = e.widget
1045
+
1046
+ # Get the class name
1044
1047
  protClassName = tree.getFirst().split('.')[-1]
1048
+
1049
+ # Get the class: Now it may not be installed!!
1045
1050
  protClass = self.domain.getProtocols().get(protClassName)
1046
- prot = self.project.newProtocol(protClass)
1047
- self._openProtocolForm(prot, disableRunMode=True, position=position)
1051
+
1052
+ # If found continue to open the protocol form to ask for parameters
1053
+ if protClass is not None:
1054
+ prot = self.project.newProtocol(protClass)
1055
+ self._openProtocolForm(prot, disableRunMode=True, position=position, previousProt=self.getSelectedProtocol())
1056
+ # Missing class: probably not installed. Inform
1057
+ else:
1058
+ # Get the value as populated in pyworkflow.gui.project.searchprotocol.py:235 comming from SearchProtocolWindow.addSuggestions
1059
+ rowValues = tree.item(protClassName)["values"]
1060
+ prot_label = rowValues[0]
1061
+ installedMsg = rowValues[2]
1062
+ msg = ("%s %s To get it use the plugin manager or installation "
1063
+ "command and restart Scipion. See %s .") % (prot_label, installedMsg, DOCSITEURLS.PLUGIN_MANAGER)
1064
+ showInfo("%s protocol is missing." % prot_label,
1065
+ msg, self)
1048
1066
 
1049
1067
  def _toggleColorScheme(self, e=None):
1050
1068
 
@@ -1332,13 +1350,20 @@ class ProtocolsView(tk.Frame):
1332
1350
  #
1333
1351
  # if update is not None: self._updateSelection()
1334
1352
 
1335
- def _openProtocolForm(self, prot, disableRunMode=False, position=None):
1336
- """Open the Protocol GUI Form given a Protocol instance"""
1353
+ def _openProtocolForm(self, prot, disableRunMode=False, position=None, previousProt=None):
1354
+ """Open the Protocol GUI Form given a Protocol instance
1355
+
1356
+ :param prot: protocol to show and edit its parameters
1357
+ :param disableRunMode: to show the form it in read only mode
1358
+ :param position: Optional. Position to add the box once added to the graph.
1359
+ :param previousProt: Optional. If passed it will try to link this protocol to the previous protocol.
1360
+
1361
+ """
1337
1362
 
1338
1363
  w = FormWindow(Message.TITLE_NAME_RUN + prot.getClassName(),
1339
1364
  prot, self._executeSaveProtocol, self.window,
1340
1365
  updateProtocolCallback=self._updateProtocol,
1341
- disableRunMode=disableRunMode, position=position)
1366
+ disableRunMode=disableRunMode, position=position, previousProt=previousProt)
1342
1367
  w.adjustSize()
1343
1368
  w.show(center=True)
1344
1369
 
@@ -1495,7 +1520,7 @@ class ProtocolsView(tk.Frame):
1495
1520
 
1496
1521
  def _scheduleRunsUpdate(self, secs=1, position=None):
1497
1522
  # self.runsTree.after(secs*1000, self.refreshRuns)
1498
- self.window.enqueue(lambda : self.refreshRuns(position=position))
1523
+ self.window.enqueue(lambda: self.refreshRuns(position=position))
1499
1524
 
1500
1525
  def executeProtocol(self, prot):
1501
1526
  """ Function to execute a protocol called not
@@ -1509,7 +1534,6 @@ class ProtocolsView(tk.Frame):
1509
1534
  if onlySave:
1510
1535
  self.project.saveProtocol(prot)
1511
1536
  msg = Message.LABEL_SAVED_FORM
1512
- # msg = "Protocol successfully saved."
1513
1537
 
1514
1538
  else:
1515
1539
  if doSchedule:
@@ -1596,7 +1620,6 @@ class ProtocolsView(tk.Frame):
1596
1620
  def _copyProtocolsToClipboard(self, e=None):
1597
1621
 
1598
1622
  protocols = self._getSelectedProtocols()
1599
-
1600
1623
  jsonStr = self.project.getProtocolsJson(protocols)
1601
1624
 
1602
1625
  self.clipboard_clear()
@@ -1865,13 +1888,13 @@ class ProtocolsView(tk.Frame):
1865
1888
  self._lastStatus = None # force logs to re-load
1866
1889
  self._scheduleRunsUpdate()
1867
1890
 
1868
- def _analyzeResults(self, prot, keyPressed):
1869
- viewers = self.domain.findViewers(prot.getClassName(), DESKTOP_TKINTER)
1891
+ def _analyzeResults(self, prot:Protocol, keyPressed):
1892
+ viewers = self.domain.findViewers(prot, DESKTOP_TKINTER)
1870
1893
  if len(viewers):
1871
1894
  # Instantiate the first available viewer
1872
- # TODO: If there are more than one viewer we should display
1873
- # TODO: a selection menu
1874
- firstViewer = viewers[0](project=self.project, protocol=prot,
1895
+ viewer = viewers[0]
1896
+ logger.info("Specific viewer found for protocol %s: %s" % (prot.getRunName, viewer))
1897
+ firstViewer = viewer(project=self.project, protocol=prot,
1875
1898
  parent=self.window, keyPressed=keyPressed)
1876
1899
 
1877
1900
  if isinstance(firstViewer, ProtocolViewer):
@@ -1884,7 +1907,7 @@ class ProtocolsView(tk.Frame):
1884
1907
  outputList.append(output)
1885
1908
 
1886
1909
  for output in outputList:
1887
- viewers = self.domain.findViewers(output.getClassName(), DESKTOP_TKINTER)
1910
+ viewers = self.domain.findViewers(output, DESKTOP_TKINTER)
1888
1911
  if len(viewers):
1889
1912
  # Instantiate the first available viewer
1890
1913
  # TODO: If there are more than one viewer we should display
@@ -27,8 +27,12 @@ import json
27
27
  import os
28
28
  from configparser import ConfigParser
29
29
 
30
+ import logging
31
+ logger = logging.getLogger(__name__)
32
+
33
+
30
34
  from pyworkflow import Config
31
- import pyworkflow.gui as pwgui
35
+ import pyworkflow.gui as pwgui
32
36
  import pyworkflow.object as pwobj
33
37
  import pyworkflow.utils as pwutils
34
38
  from pyworkflow.gui.project.utils import isAFinalProtocol
@@ -113,7 +117,7 @@ class RunIOTreeProvider(pwgui.tree.TreeProvider):
113
117
  % objLabel):
114
118
  prot.getProject().deleteProtocolOutput(prot, obj)
115
119
  self.parent._fillSummary()
116
- self.parent.window.showInfo("Object *%s* successfully deleted."
120
+ logger.info("Object *%s* successfully deleted upon user request."
117
121
  % objLabel)
118
122
  except Exception as ex:
119
123
  self.parent.window.showError(str(ex))
@@ -138,7 +142,7 @@ class RunIOTreeProvider(pwgui.tree.TreeProvider):
138
142
  self._loggerCallback("Discovering viewers for the first time across all the plugins.")
139
143
 
140
144
 
141
- viewers = Config.getDomain().findViewers(obj.getClassName(), DESKTOP_TKINTER)
145
+ viewers = Config.getDomain().findViewers(obj, DESKTOP_TKINTER)
142
146
 
143
147
  def viewerCallback(viewer):
144
148
  return lambda: self._visualizeObject(viewer, obj)
@@ -191,7 +195,7 @@ class RunIOTreeProvider(pwgui.tree.TreeProvider):
191
195
  try:
192
196
  value = str(label)
193
197
  except Exception as e:
194
- print("Can not convert object %s - %s to string." % (key, name))
198
+ logger.info("Can not convert object %s - %s to string." % (key, name))
195
199
  value = str(e)
196
200
 
197
201
  return value
@@ -502,8 +506,7 @@ class ProtocolTreeConfig:
502
506
  cls.__addProtocolsFromConf(protocols, protocolsConfPath)
503
507
 
504
508
  except Exception as e:
505
- print('Failed to read settings. The reported error was:\n %s\n'
506
- 'To solve it, fix %s and run again.' % (e, pluginName))
509
+ logger.info(f'Failed to read protocols.conf from {pluginName}. The reported error was:\n {e}\n')
507
510
 
508
511
  # Clean empty sections
509
512
  cls._hideEmptySections(protocols)
pyworkflow/gui/widgets.py CHANGED
@@ -281,8 +281,8 @@ class GradientFrame(tk.Canvas):
281
281
 
282
282
  def __init__(self, parent, **args):
283
283
  tk.Canvas.__init__(self, parent, **args)
284
- self._color1 = Config.SCIPION_BG_COLOR #"#d2a7a7"
285
- self._color2 = Config.SCIPION_MAIN_COLOR #"#820808"
284
+ self._color1 = Config.SCIPION_BG_COLOR
285
+ self._color2 = Config.SCIPION_MAIN_COLOR
286
286
  self.bind("<Configure>", self._draw_gradient)
287
287
 
288
288
  def _draw_gradient(self, event=None):
@@ -1543,18 +1543,18 @@ class SqliteFlatDb(SqliteDb):
1543
1543
  return self._results(iterate=False)
1544
1544
 
1545
1545
  def count(self):
1546
- """ Return the number of element in the table. """
1547
- self.executeCommand(self.selectCmd('1').replace('*', 'COUNT(id)'))
1546
+ """ Return the number of elements in the table. """
1547
+ self.executeCommand(self.selectCmd(None, orderByStr="").replace('*', 'COUNT(*)'))
1548
1548
  return self.cursor.fetchone()[0]
1549
1549
 
1550
1550
  def maxId(self):
1551
1551
  """ Return the maximum id from the Objects table. """
1552
- self.executeCommand(self.selectCmd('1').replace('*', 'MAX(id)'))
1552
+ self.executeCommand(self.selectCmd(None, orderByStr="").replace('*', 'MAX(id)'))
1553
1553
  return self.cursor.fetchone()[0]
1554
1554
 
1555
1555
  # FIXME: Seems to be duplicated and a subset of selectAll
1556
1556
  def selectObjectsBy(self, iterate=False, **args):
1557
- """More flexible select where the constrains can be passed
1557
+ """More flexible select where the constraints can be passed
1558
1558
  as a dictionary, the concatenation is done by an AND"""
1559
1559
  whereList = ['%s=?' % k for k in args.keys()]
1560
1560
  whereStr = ' AND '.join(whereList)
pyworkflow/plugin.py CHANGED
@@ -28,6 +28,7 @@
28
28
  # **************************************************************************
29
29
  import logging
30
30
  import sys
31
+ from functools import lru_cache
31
32
 
32
33
  from pyworkflow import Variable, VariablesRegistry, VarTypes
33
34
  from .protocol import Protocol
@@ -79,6 +80,9 @@ class Domain:
79
80
  _viewers = {}
80
81
  _wizards = {}
81
82
 
83
+ # Preferred viewers:
84
+ _preferred_viewers = None
85
+
82
86
  @classmethod
83
87
  def registerPlugin(cls, name):
84
88
  """ Register a new plugin. This function should only be called when
@@ -113,15 +117,17 @@ class Domain:
113
117
 
114
118
  # Catch any import exception, warn about it but continue.
115
119
  except ModuleNotFoundError as e:
120
+
121
+ logger.debug("Module %s not found: %s" %(name, e))
116
122
  if e.name == name:
117
123
  # This is probably due to a priority package like pwchem not being installed
118
- pass
124
+ logger.debug("Name is different!!: e.name='%s', name='%s'" %( e.name , name))
119
125
  else:
120
126
  logger.warning("Plugin '%s' has import errors: %s. Maybe a missing dependency?. "
121
127
  "Is it devel mode and need to be reinstalled?. Ignoring it and continuing." % (name, str(e)))
122
128
  except Exception as e:
123
129
 
124
- (pwutils.yellow("WARNING!!: Plugin containing module %s does not import properly. "
130
+ logger.info(pwutils.yellow("WARNING!!: Plugin containing module %s does not import properly. "
125
131
  "All its content will be missing in this execution." % name))
126
132
  logger.info("Please, contact developers at %s and send this ugly information below. They'll understand it!." % DOCSITEURLS.CONTACTUS)
127
133
  logger.info("Error message: %s"% str(e))
@@ -390,56 +396,99 @@ class Domain:
390
396
  @classmethod
391
397
  def getPreferredViewers(cls, className):
392
398
  """ Find and import the preferred viewers for this class. """
393
- viewerNames = pw.Config.VIEWERS.get(className, [])
394
- if not isinstance(viewerNames, list):
395
- viewerNames = [viewerNames]
396
- viewers = [] # we will try to import them and store here
397
- for prefViewerStr in viewerNames:
398
- try:
399
- viewerModule, viewerClassName = prefViewerStr.rsplit('.', 1)
400
- prefViewer = cls.importFromPlugin(viewerModule,
401
- viewerClassName,
402
- doRaise=True)
403
- viewers.append(prefViewer)
404
- except Exception as e:
405
- logger.error("Couldn't load \"%s\" as preferred viewer for %s.\n"
406
- "There might be a typo in your VIEWERS variable "
407
- "or an error in the viewer's plugin installation"
408
- % (prefViewerStr, className), exc_info=e)
409
- return viewers
399
+
400
+ if cls._preferred_viewers is None:
401
+ logger.info("Caching preferred viewers from VIEWERS config variable.")
402
+ cls._preferred_viewers = dict()
403
+
404
+ for target, viewerNames in pw.Config.VIEWERS.items():
405
+ viewers = []
406
+ for prefViewerStr in viewerNames:
407
+ try:
408
+ viewerModule, viewerClassName = prefViewerStr.rsplit('.', 1)
409
+ prefViewer = cls.importFromPlugin(viewerModule,
410
+ viewerClassName,
411
+ doRaise=True)
412
+ viewers.append(prefViewer)
413
+ except Exception as e:
414
+ logger.error("Couldn't load \"%s\" as preferred viewer for %s.\n"
415
+ "There might be a typo in your VIEWERS variable "
416
+ "or an error in the viewer's plugin installation"
417
+ % (prefViewerStr, className), exc_info=e)
418
+
419
+ cls._preferred_viewers[target] = viewers
420
+
421
+ return cls._preferred_viewers.get(className, [])
422
+
423
+ @classmethod
424
+ @lru_cache
425
+ def getViewersSorted(cls):
426
+ """ Returns all viewers sorted by its class name"""
427
+
428
+ viewers = cls.getViewers()
429
+ return [viewers[key] for key in sorted(viewers)]
410
430
 
411
431
  @classmethod
412
- def findViewers(cls, className, environment):
413
- """ Find the available viewers in this Domain for this class. """
432
+ def findViewers(cls, target, environment):
433
+ """ Find the available viewers in this Domain for this target.
434
+
435
+ Sorting criteria:
436
+
437
+ 1st Will appear those viewers in VIEWERS variable (preferred viewers) in appearance order
438
+ 2nd Viewers targeting specifically the target and not any super class of it
439
+ 3rd rest.
440
+
441
+ In 2nd and 3rd case the order viewers are added depends on the alphabetical order the viewer when discovered.
442
+ This usually matches the viewer class name, but could be fine tune using import aliases like:
443
+
444
+ from my_viewer import MyViewer as AAAMyViewer in the viewers folder of the plugin.
445
+
446
+ In case viewers is a file and not a folder with an __init__, I'm afraid class name is what is taken into account
447
+
448
+ The import order is not possible to use since python sort them automatically.
449
+
450
+ """
414
451
  viewers = []
415
452
  try:
416
- clazz = cls.findClass(className)
453
+ instance = None
454
+ if isinstance(target, str):
455
+ logger.warning("DEVELOPERS: pass the instance/object instead of the class. This mode will be deprecated soon")
456
+ className = target
457
+ clazz = cls.findClass(className)
458
+ else:
459
+ className = target.__class__.__name__
460
+ clazz = target.__class__
461
+ instance = target
462
+
417
463
  baseClasses = clazz.mro()
418
- preferredViewers = cls.getPreferredViewers(className)
419
- preferedFlag = 0
420
464
 
421
- for viewer in cls.getViewers().values():
422
- viewerAdded=False
465
+ # Add preferred viewers
466
+ preferred_viewers = []
467
+ available_preferred_viewers = cls.getPreferredViewers(className)
468
+ for prefViewer in available_preferred_viewers:
469
+ if prefViewer.can_handle_this(baseClasses, instance=instance):
470
+ preferred_viewers.append(prefViewer)
471
+
472
+
473
+ specific_viewers = []
474
+ other_viewers=[]
475
+ # get all the viewers available
476
+
477
+ for viewer in cls.getViewersSorted():
478
+
423
479
  if environment in viewer._environments:
424
- for t in viewer._targets:
425
- if t in baseClasses:
426
- for prefViewer in preferredViewers:
427
- if viewer is prefViewer:
428
- viewers.insert(0, viewer)
429
- viewerAdded=True
430
- preferedFlag = 1
431
- break
480
+
481
+ if viewer not in preferred_viewers:
482
+
483
+ t = viewer.can_handle_this(baseClasses, target)
484
+ if t is not None:
485
+ if t == clazz:
486
+ specific_viewers.append(viewer)
487
+
432
488
  else:
433
- if t == clazz:
434
- viewers.insert(preferedFlag, viewer)
435
- viewerAdded = True
436
- else:
437
- viewers.append(viewer)
438
- viewerAdded = True
439
- break
440
-
441
- if viewerAdded:
442
- break
489
+ other_viewers.append(viewer)
490
+
491
+ viewers = preferred_viewers + specific_viewers + other_viewers
443
492
 
444
493
  except Exception as e:
445
494
  # Catch if there is a missing plugin, we will get Legacy which triggers and Exception