scipion-pyworkflow 3.7.0__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.
- pyworkflow/__init__.py +33 -0
- pyworkflow/apps/__init__.py +29 -0
- pyworkflow/apps/pw_manager.py +37 -0
- pyworkflow/apps/pw_plot.py +51 -0
- pyworkflow/apps/pw_project.py +113 -0
- pyworkflow/apps/pw_protocol_list.py +143 -0
- pyworkflow/apps/pw_protocol_run.py +51 -0
- pyworkflow/apps/pw_run_tests.py +267 -0
- pyworkflow/apps/pw_schedule_run.py +322 -0
- pyworkflow/apps/pw_sleep.py +37 -0
- pyworkflow/apps/pw_sync_data.py +439 -0
- pyworkflow/apps/pw_viewer.py +78 -0
- pyworkflow/config.py +536 -0
- pyworkflow/constants.py +212 -0
- pyworkflow/exceptions.py +18 -0
- pyworkflow/gui/__init__.py +36 -0
- pyworkflow/gui/browser.py +726 -0
- pyworkflow/gui/canvas.py +1190 -0
- pyworkflow/gui/dialog.py +976 -0
- pyworkflow/gui/form.py +2627 -0
- pyworkflow/gui/graph.py +247 -0
- pyworkflow/gui/graph_layout.py +271 -0
- pyworkflow/gui/gui.py +566 -0
- pyworkflow/gui/matplotlib_image.py +233 -0
- pyworkflow/gui/plotter.py +247 -0
- pyworkflow/gui/project/__init__.py +25 -0
- pyworkflow/gui/project/base.py +192 -0
- pyworkflow/gui/project/constants.py +139 -0
- pyworkflow/gui/project/labels.py +205 -0
- pyworkflow/gui/project/project.py +484 -0
- pyworkflow/gui/project/searchprotocol.py +154 -0
- pyworkflow/gui/project/searchrun.py +181 -0
- pyworkflow/gui/project/steps.py +166 -0
- pyworkflow/gui/project/utils.py +332 -0
- pyworkflow/gui/project/variables.py +179 -0
- pyworkflow/gui/project/viewdata.py +472 -0
- pyworkflow/gui/project/viewprojects.py +510 -0
- pyworkflow/gui/project/viewprotocols.py +2093 -0
- pyworkflow/gui/project/viewprotocols_extra.py +560 -0
- pyworkflow/gui/text.py +771 -0
- pyworkflow/gui/tooltip.py +185 -0
- pyworkflow/gui/tree.py +684 -0
- pyworkflow/gui/widgets.py +307 -0
- pyworkflow/mapper/__init__.py +26 -0
- pyworkflow/mapper/mapper.py +222 -0
- pyworkflow/mapper/sqlite.py +1578 -0
- pyworkflow/mapper/sqlite_db.py +145 -0
- pyworkflow/object.py +1512 -0
- pyworkflow/plugin.py +712 -0
- pyworkflow/project/__init__.py +31 -0
- pyworkflow/project/config.py +451 -0
- pyworkflow/project/manager.py +179 -0
- pyworkflow/project/project.py +1990 -0
- pyworkflow/project/scripts/clean_projects.py +77 -0
- pyworkflow/project/scripts/config.py +92 -0
- pyworkflow/project/scripts/create.py +77 -0
- pyworkflow/project/scripts/edit_workflow.py +90 -0
- pyworkflow/project/scripts/fix_links.py +39 -0
- pyworkflow/project/scripts/load.py +87 -0
- pyworkflow/project/scripts/refresh.py +83 -0
- pyworkflow/project/scripts/schedule.py +111 -0
- pyworkflow/project/scripts/stack2volume.py +41 -0
- pyworkflow/project/scripts/stop.py +81 -0
- pyworkflow/protocol/__init__.py +38 -0
- pyworkflow/protocol/bibtex.py +48 -0
- pyworkflow/protocol/constants.py +86 -0
- pyworkflow/protocol/executor.py +334 -0
- pyworkflow/protocol/hosts.py +313 -0
- pyworkflow/protocol/launch.py +270 -0
- pyworkflow/protocol/package.py +42 -0
- pyworkflow/protocol/params.py +744 -0
- pyworkflow/protocol/protocol.py +2554 -0
- pyworkflow/resources/Imagej.png +0 -0
- pyworkflow/resources/chimera.png +0 -0
- pyworkflow/resources/fa-exclamation-triangle_alert.png +0 -0
- pyworkflow/resources/fa-info-circle_alert.png +0 -0
- pyworkflow/resources/fa-search.png +0 -0
- pyworkflow/resources/fa-times-circle_alert.png +0 -0
- pyworkflow/resources/file_vol.png +0 -0
- pyworkflow/resources/loading.gif +0 -0
- pyworkflow/resources/no-image128.png +0 -0
- pyworkflow/resources/scipion_bn.png +0 -0
- pyworkflow/resources/scipion_icon.png +0 -0
- pyworkflow/resources/scipion_icon.svg +397 -0
- pyworkflow/resources/scipion_icon_proj.png +0 -0
- pyworkflow/resources/scipion_icon_projs.png +0 -0
- pyworkflow/resources/scipion_icon_prot.png +0 -0
- pyworkflow/resources/scipion_logo.png +0 -0
- pyworkflow/resources/scipion_logo_normal.png +0 -0
- pyworkflow/resources/scipion_logo_small.png +0 -0
- pyworkflow/resources/sprites.png +0 -0
- pyworkflow/resources/sprites.xcf +0 -0
- pyworkflow/resources/wait.gif +0 -0
- pyworkflow/template.py +322 -0
- pyworkflow/tests/__init__.py +29 -0
- pyworkflow/tests/test_utils.py +25 -0
- pyworkflow/tests/tests.py +341 -0
- pyworkflow/utils/__init__.py +38 -0
- pyworkflow/utils/dataset.py +414 -0
- pyworkflow/utils/echo.py +104 -0
- pyworkflow/utils/graph.py +196 -0
- pyworkflow/utils/log.py +284 -0
- pyworkflow/utils/path.py +527 -0
- pyworkflow/utils/process.py +132 -0
- pyworkflow/utils/profiler.py +92 -0
- pyworkflow/utils/progressbar.py +154 -0
- pyworkflow/utils/properties.py +627 -0
- pyworkflow/utils/reflection.py +129 -0
- pyworkflow/utils/utils.py +877 -0
- pyworkflow/utils/which.py +229 -0
- pyworkflow/viewer.py +328 -0
- pyworkflow/webservices/__init__.py +8 -0
- pyworkflow/webservices/config.py +11 -0
- pyworkflow/webservices/notifier.py +162 -0
- pyworkflow/webservices/repository.py +59 -0
- pyworkflow/webservices/workflowhub.py +74 -0
- pyworkflow/wizard.py +64 -0
- pyworkflowtests/__init__.py +51 -0
- pyworkflowtests/bibtex.py +51 -0
- pyworkflowtests/objects.py +830 -0
- pyworkflowtests/protocols.py +154 -0
- pyworkflowtests/tests/__init__.py +0 -0
- pyworkflowtests/tests/test_canvas.py +72 -0
- pyworkflowtests/tests/test_domain.py +45 -0
- pyworkflowtests/tests/test_logs.py +74 -0
- pyworkflowtests/tests/test_mappers.py +392 -0
- pyworkflowtests/tests/test_object.py +507 -0
- pyworkflowtests/tests/test_project.py +42 -0
- pyworkflowtests/tests/test_protocol_execution.py +72 -0
- pyworkflowtests/tests/test_protocol_export.py +78 -0
- pyworkflowtests/tests/test_protocol_output.py +158 -0
- pyworkflowtests/tests/test_streaming.py +47 -0
- pyworkflowtests/tests/test_utils.py +210 -0
- scipion_pyworkflow-3.7.0.dist-info/LICENSE.txt +674 -0
- scipion_pyworkflow-3.7.0.dist-info/METADATA +107 -0
- scipion_pyworkflow-3.7.0.dist-info/RECORD +140 -0
- scipion_pyworkflow-3.7.0.dist-info/WHEEL +5 -0
- scipion_pyworkflow-3.7.0.dist-info/dependency_links.txt +1 -0
- scipion_pyworkflow-3.7.0.dist-info/entry_points.txt +5 -0
- scipion_pyworkflow-3.7.0.dist-info/top_level.txt +2 -0
pyworkflow/plugin.py
ADDED
@@ -0,0 +1,712 @@
|
|
1
|
+
# **************************************************************************
|
2
|
+
# *
|
3
|
+
# * Authors: Yaiza Rancel (cyrancel@cnb.csic.es) [1]
|
4
|
+
# * Pablo Conesa (pconesa@cnb.csic.es) [1]
|
5
|
+
# * J.M. De la Rosa Trevin (delarosatrevin@scilifelab.se) [2]
|
6
|
+
# *
|
7
|
+
# * [1] Unidad de Bioinformatica of Centro Nacional de Biotecnologia , CSIC
|
8
|
+
# * [2] SciLifeLab, Stockholm University
|
9
|
+
# *
|
10
|
+
# * This program is free software; you can redistribute it and/or modify
|
11
|
+
# * it under the terms of the GNU General Public License as published by
|
12
|
+
# * the Free Software Foundation; either version 3 of the License, or
|
13
|
+
# * (at your option) any later version.
|
14
|
+
# *
|
15
|
+
# * This program is distributed in the hope that it will be useful,
|
16
|
+
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
17
|
+
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
18
|
+
# * GNU General Public License for more details.
|
19
|
+
# *
|
20
|
+
# * You should have received a copy of the GNU General Public License
|
21
|
+
# * along with this program; if not, write to the Free Software
|
22
|
+
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
|
23
|
+
# * 02111-1307 USA
|
24
|
+
# *
|
25
|
+
# * All comments concerning this program package may be sent to the
|
26
|
+
# * e-mail address 'scipion@cnb.csic.es'
|
27
|
+
# *
|
28
|
+
# **************************************************************************
|
29
|
+
import logging
|
30
|
+
import sys
|
31
|
+
|
32
|
+
from pyworkflow import Variable, VariablesRegistry, VarTypes
|
33
|
+
from .protocol import Protocol
|
34
|
+
from .viewer import Viewer
|
35
|
+
from .wizard import Wizard
|
36
|
+
|
37
|
+
logger = logging.getLogger(__name__)
|
38
|
+
import glob
|
39
|
+
import os
|
40
|
+
import importlib
|
41
|
+
import inspect
|
42
|
+
import traceback
|
43
|
+
import types
|
44
|
+
from abc import ABCMeta, abstractmethod
|
45
|
+
|
46
|
+
import pyworkflow as pw
|
47
|
+
import pyworkflow.utils as pwutils
|
48
|
+
import pyworkflow.object as pwobj
|
49
|
+
from pyworkflow.template import LocalTemplate
|
50
|
+
from pyworkflow.utils import sortListByList
|
51
|
+
|
52
|
+
try:
|
53
|
+
import importlib_metadata
|
54
|
+
except ModuleNotFoundError:
|
55
|
+
raise ModuleNotFoundError('You are missing importlib-metadata package. '
|
56
|
+
'Please run: scipion3 pip install importlib-metadata')
|
57
|
+
|
58
|
+
from .constants import *
|
59
|
+
|
60
|
+
|
61
|
+
class Domain:
|
62
|
+
""" Class to represent the application domain.
|
63
|
+
It will allow to specify new objects, protocols, viewers and wizards
|
64
|
+
through the registration of new plugins.
|
65
|
+
"""
|
66
|
+
|
67
|
+
# The following classes should be defined in subclasses of Domain
|
68
|
+
_name = __name__
|
69
|
+
_protocolClass = Protocol
|
70
|
+
_objectClass = pwobj.Object
|
71
|
+
_viewerClass = Viewer
|
72
|
+
_wizardClass = Wizard
|
73
|
+
_baseClasses = {} # Update this with the base classes of the Domain
|
74
|
+
|
75
|
+
# Dictionaries to store different type of objects
|
76
|
+
_plugins = {}
|
77
|
+
_protocols = {}
|
78
|
+
_objects = {}
|
79
|
+
_viewers = {}
|
80
|
+
_wizards = {}
|
81
|
+
|
82
|
+
@classmethod
|
83
|
+
def registerPlugin(cls, name):
|
84
|
+
""" Register a new plugin. This function should only be called when
|
85
|
+
creating a class with __metaclass__=PluginMeta that will trigger this.
|
86
|
+
"""
|
87
|
+
try:
|
88
|
+
m = importlib.import_module(name)
|
89
|
+
|
90
|
+
# Define variables
|
91
|
+
m._pluginInstance = m.Plugin()
|
92
|
+
# This needs an explanation. _defineVariables() are classmethods, therfore we need a class variable and thus .name can't be used.
|
93
|
+
# Ideally, since Plugin class is instantiated we could transform _defineVariables methods into instance methods and only then we
|
94
|
+
# could use .name.
|
95
|
+
Plugin._tmpName = name
|
96
|
+
m._pluginInstance.name = name
|
97
|
+
m._pluginInstance._defineVariables()
|
98
|
+
|
99
|
+
m.Domain = cls # Register the domain class for this module
|
100
|
+
# TODO avoid loading bibtex here and make a lazy load like the rest.
|
101
|
+
# Load bibtex
|
102
|
+
m._bibtex = {}
|
103
|
+
bib = cls.__getSubmodule(name, 'bibtex')
|
104
|
+
if bib is not None:
|
105
|
+
if hasattr(bib, "_bibtex"):
|
106
|
+
logger.info("WARNING FOR DEVELOPERS: %s/%s._bibtex unnecessarily declared. Just the doc string is enough." % (name, "bibtex"))
|
107
|
+
else:
|
108
|
+
try:
|
109
|
+
m._bibtex = pwutils.LazyDict(lambda: pwutils.parseBibTex(bib.__doc__))
|
110
|
+
except Exception:
|
111
|
+
pass
|
112
|
+
cls._plugins[name] = m # Register the name to as a plugin
|
113
|
+
|
114
|
+
# Catch any import exception, warn about it but continue.
|
115
|
+
except ModuleNotFoundError:
|
116
|
+
# This is probably due to a priority package like pwchem not being installed
|
117
|
+
pass
|
118
|
+
except Exception as e:
|
119
|
+
|
120
|
+
(pwutils.yellow("WARNING!!: Plugin containing module %s does not import properly. "
|
121
|
+
"All its content will be missing in this execution." % name))
|
122
|
+
logger.info("Please, contact developers at %s and send this ugly information below. They'll understand it!." % DOCSITEURLS.CONTACTUS)
|
123
|
+
logger.info(pwutils.yellow(traceback.format_exc()))
|
124
|
+
|
125
|
+
@classmethod
|
126
|
+
def getPlugins(cls):
|
127
|
+
""" Return existing plugins for this Domain. """
|
128
|
+
loaded = getattr(cls, '_pluginsLoaded', False)
|
129
|
+
if not loaded: # Load plugin only once
|
130
|
+
cls._discoverPlugins()
|
131
|
+
cls._pluginsLoaded = True
|
132
|
+
return dict(cls._plugins)
|
133
|
+
|
134
|
+
@classmethod
|
135
|
+
def _discoverPlugins(cls):
|
136
|
+
# Get the list of plugins registered
|
137
|
+
plugin_modules = importlib_metadata.entry_points(group='pyworkflow.plugin')
|
138
|
+
|
139
|
+
# Sort the list taking into account the priority
|
140
|
+
plugin_modules = sortListByList(plugin_modules.names, pw.Config.getPriorityPackageList())
|
141
|
+
|
142
|
+
for module in plugin_modules:
|
143
|
+
cls.registerPlugin(module)
|
144
|
+
|
145
|
+
@classmethod
|
146
|
+
def _discoverGUIPlugins(cls):
|
147
|
+
for entry_point in importlib_metadata.entry_points(group='pyworkflow.guiplugin'):
|
148
|
+
try:
|
149
|
+
entry_point.load()
|
150
|
+
except Exception as e:
|
151
|
+
logger.warning("Can't import %s GUI plugin: %s" % (entry_point, e))
|
152
|
+
|
153
|
+
@classmethod
|
154
|
+
def getPluginModule(cls, name):
|
155
|
+
""" Return the root of a plugin module initialized properly """
|
156
|
+
cls.registerPlugin(name)
|
157
|
+
|
158
|
+
return cls._plugins[name]
|
159
|
+
|
160
|
+
@classmethod
|
161
|
+
def getPlugin(cls, name):
|
162
|
+
logger.warning("This method will return the plugin class in the future. Please use getPluginModule instead")
|
163
|
+
return cls.getPluginModule(name)
|
164
|
+
|
165
|
+
@classmethod
|
166
|
+
def refreshPlugin(cls, name):
|
167
|
+
""" Refresh a given plugin name. """
|
168
|
+
plugin = cls.getPlugin(name)
|
169
|
+
if plugin is not None:
|
170
|
+
fn = plugin.__file__
|
171
|
+
fn_dir = os.path.dirname(fn) + os.sep
|
172
|
+
module_visit = {plugin}
|
173
|
+
module_visit_path = {fn}
|
174
|
+
del fn
|
175
|
+
|
176
|
+
def get_recursive_modules(module):
|
177
|
+
""" Get all plugin modules recursively """
|
178
|
+
for module_child in vars(module).values():
|
179
|
+
if isinstance(module_child, types.ModuleType):
|
180
|
+
fn_child = getattr(module_child, "__file__", None)
|
181
|
+
if (fn_child is not None) and fn_child.startswith(
|
182
|
+
fn_dir):
|
183
|
+
if fn_child not in module_visit_path:
|
184
|
+
module_visit.add(module_child)
|
185
|
+
module_visit_path.add(fn_child)
|
186
|
+
get_recursive_modules(module_child)
|
187
|
+
|
188
|
+
get_recursive_modules(plugin)
|
189
|
+
# reload all plugin modules
|
190
|
+
|
191
|
+
while module_visit:
|
192
|
+
for module in module_visit:
|
193
|
+
try:
|
194
|
+
importlib.reload(module)
|
195
|
+
module_visit.remove(module)
|
196
|
+
break
|
197
|
+
except Exception as ex:
|
198
|
+
pass
|
199
|
+
|
200
|
+
@classmethod
|
201
|
+
def __getSubclasses(cls, submoduleName, BaseClass,
|
202
|
+
updateBaseClasses=False, setPackage=False):
|
203
|
+
""" Load all detected subclasses of a given BaseClass.
|
204
|
+
Params:
|
205
|
+
updateBaseClasses: if True, it will try to load classes from the
|
206
|
+
Domain submodule that was not imported as globals()
|
207
|
+
"""
|
208
|
+
subclasses = getattr(cls, '_%s' % submoduleName)
|
209
|
+
|
210
|
+
if not subclasses: # Only discover subclasses once
|
211
|
+
if updateBaseClasses:
|
212
|
+
sub = cls.__getSubmodule(cls.getName(), submoduleName)
|
213
|
+
if sub is not None:
|
214
|
+
for name in cls.getModuleClasses(sub):
|
215
|
+
attr = getattr(sub, name)
|
216
|
+
if inspect.isclass(attr) and issubclass(attr, BaseClass):
|
217
|
+
cls._baseClasses[name] = attr
|
218
|
+
|
219
|
+
for pluginName, module in cls.getPlugins().items():
|
220
|
+
sub = cls.__getSubmodule(pluginName, submoduleName)
|
221
|
+
if sub is not None:
|
222
|
+
for name in cls.getModuleClasses(sub):
|
223
|
+
attr = getattr(sub, name)
|
224
|
+
if inspect.isclass(attr) and issubclass(attr, BaseClass):
|
225
|
+
|
226
|
+
# Check if the class already exists (to prevent
|
227
|
+
# naming collisions)
|
228
|
+
if name in subclasses:
|
229
|
+
# Get already added class plugin
|
230
|
+
pluginCollision = subclasses[name]._package.__name__
|
231
|
+
logger.info("ERROR: Name collision (%s) detected "
|
232
|
+
"while discovering %s.%s.\n"
|
233
|
+
" It conflicts with %s" %
|
234
|
+
(name, pluginName, submoduleName,
|
235
|
+
pluginCollision))
|
236
|
+
else:
|
237
|
+
# Set this special property used by Scipion
|
238
|
+
# Protocols need the package to be set
|
239
|
+
if setPackage:
|
240
|
+
|
241
|
+
attr._plugin = getattr(module, "_pluginInstance", None)
|
242
|
+
attr._package = module
|
243
|
+
|
244
|
+
subclasses[name] = attr
|
245
|
+
subclasses.update(
|
246
|
+
pwutils.getSubclasses(BaseClass, cls._baseClasses))
|
247
|
+
|
248
|
+
return subclasses
|
249
|
+
|
250
|
+
@classmethod
|
251
|
+
def getModuleClasses(cls, module):
|
252
|
+
# Dir was used before but dir returns all imported elements
|
253
|
+
# included those imported to be BaseClasses.
|
254
|
+
# return dir(module)
|
255
|
+
|
256
|
+
# Get the module name
|
257
|
+
moduleName = module.__name__
|
258
|
+
|
259
|
+
# Get any module class
|
260
|
+
for name, declaredClass in inspect.getmembers(module, inspect.isclass):
|
261
|
+
if moduleName in declaredClass.__module__:
|
262
|
+
yield name
|
263
|
+
|
264
|
+
@classmethod
|
265
|
+
def getProtocols(cls):
|
266
|
+
""" Return all Protocol subclasses from all plugins for this domain."""
|
267
|
+
return cls.__getSubclasses('protocols', cls._protocolClass, setPackage=True)
|
268
|
+
|
269
|
+
@classmethod
|
270
|
+
def getObjects(cls):
|
271
|
+
""" Return all EMObject subclasses from all plugins for this domain."""
|
272
|
+
return cls.__getSubclasses('objects', cls._objectClass)
|
273
|
+
|
274
|
+
@classmethod
|
275
|
+
def viewersLoaded(cls):
|
276
|
+
""" Returns true if viewers have been already discovered"""
|
277
|
+
return len(cls._viewers) != 0
|
278
|
+
|
279
|
+
@classmethod
|
280
|
+
def getViewers(cls):
|
281
|
+
""" Return all Viewer subclasses from all plugins for this domain."""
|
282
|
+
return cls.__getSubclasses('viewers', cls._viewerClass,
|
283
|
+
updateBaseClasses=True, setPackage=True)
|
284
|
+
|
285
|
+
@classmethod
|
286
|
+
def getWizards(cls):
|
287
|
+
""" Return all Wizard subclasses from all plugins for this domain."""
|
288
|
+
return cls.__getSubclasses('wizards', cls._wizardClass)
|
289
|
+
|
290
|
+
@classmethod
|
291
|
+
def getMapperDict(cls):
|
292
|
+
""" Return a dictionary that can be used with subclasses of Mapper
|
293
|
+
to store/retrieve objects (including protocols) defined in this
|
294
|
+
Domain. """
|
295
|
+
mapperDict = getattr(cls, '__mapperDict', None)
|
296
|
+
|
297
|
+
if mapperDict is None:
|
298
|
+
mapperDict = dict(pwobj.OBJECTS_DICT)
|
299
|
+
mapperDict.update(cls.getObjects())
|
300
|
+
mapperDict.update(cls.getProtocols())
|
301
|
+
cls.__mapperDict = mapperDict
|
302
|
+
|
303
|
+
return mapperDict
|
304
|
+
|
305
|
+
@classmethod
|
306
|
+
def getName(cls):
|
307
|
+
""" Return the name of this Domain. """
|
308
|
+
return cls._name
|
309
|
+
|
310
|
+
@staticmethod
|
311
|
+
def importFromPlugin(module, objects=None, errorMsg='', doRaise=False):
|
312
|
+
"""
|
313
|
+
This method try to import either a list of objects from the
|
314
|
+
module/plugin or the whole module/plugin and returns what is
|
315
|
+
imported if not fails.
|
316
|
+
When the import fails (due to the plugin or the object is not found),
|
317
|
+
it prints a common message + optional errorMsg;
|
318
|
+
or it raise an error with the same message, if doRaise is True.
|
319
|
+
|
320
|
+
:param module: Module name to import
|
321
|
+
:param objects: Optional, string with objects to return present in module
|
322
|
+
:param errorMsg: Optional, extra error message to append to the main message.
|
323
|
+
:param doRaise: If True it will raise an exception instead of tolerating the import error
|
324
|
+
|
325
|
+
Usages::
|
326
|
+
|
327
|
+
# Import the whole plugin 'plugin1' as 'plug1'
|
328
|
+
plug1 = importFromPlugin('plugin1')
|
329
|
+
|
330
|
+
# Import a plugin's module
|
331
|
+
pl1Cons = importFromPlugin('plug1.constants')
|
332
|
+
|
333
|
+
# Import a single class from a plugin's module
|
334
|
+
p1prot1 = importFromPlugin('plug1.protocols', 'prot1')
|
335
|
+
|
336
|
+
# Import some classes from a plugin's module,
|
337
|
+
# the returned tuple has the same length of the second argument
|
338
|
+
pt1, pt2, ... = importFromPlugin('plugin1.protocols',
|
339
|
+
['pt1', 'pt2', ...])
|
340
|
+
"""
|
341
|
+
def _tryImportFromPlugin(submodule=None):
|
342
|
+
try:
|
343
|
+
if submodule is None: # Import only the module
|
344
|
+
output = importlib.import_module(module)
|
345
|
+
else: # Import the class of that module
|
346
|
+
output = getattr(importlib.import_module(module), submodule)
|
347
|
+
return output
|
348
|
+
except Exception as e:
|
349
|
+
plugName = module.split('.')[0] # The Main module is the plugin
|
350
|
+
errMsg = (str(e) if errorMsg == ''
|
351
|
+
else "%s. %s" % (str(e), errorMsg))
|
352
|
+
Domain.__pluginNotFound(plugName, errMsg, doRaise)
|
353
|
+
|
354
|
+
if objects is None or isinstance(objects, str):
|
355
|
+
output = _tryImportFromPlugin(objects)
|
356
|
+
else:
|
357
|
+
output = tuple()
|
358
|
+
for obj in objects:
|
359
|
+
output += (_tryImportFromPlugin(obj), ) # append in tuple
|
360
|
+
return output
|
361
|
+
|
362
|
+
@classmethod
|
363
|
+
def findClass(cls, className):
|
364
|
+
""" Find a class object given its name.
|
365
|
+
The search will start with protocols and then with protocols.
|
366
|
+
"""
|
367
|
+
# FIXME: Why not also Viewers, Wizards?
|
368
|
+
c = cls.getProtocols().get(className,
|
369
|
+
cls.getObjects().get(className, None))
|
370
|
+
if c is None:
|
371
|
+
raise Exception("findClass: class '%s' not found." % className)
|
372
|
+
return c
|
373
|
+
|
374
|
+
@classmethod
|
375
|
+
def findSubClasses(cls, classDict, className):
|
376
|
+
""" Find all subclasses of a give className. """
|
377
|
+
clsObj = classDict[className]
|
378
|
+
subclasses = {}
|
379
|
+
|
380
|
+
for k, v in classDict.items():
|
381
|
+
if issubclass(v, clsObj):
|
382
|
+
subclasses[k] = v
|
383
|
+
return subclasses
|
384
|
+
|
385
|
+
@classmethod
|
386
|
+
def getPreferredViewers(cls, className):
|
387
|
+
""" Find and import the preferred viewers for this class. """
|
388
|
+
viewerNames = pw.Config.VIEWERS.get(className, [])
|
389
|
+
if not isinstance(viewerNames, list):
|
390
|
+
viewerNames = [viewerNames]
|
391
|
+
viewers = [] # we will try to import them and store here
|
392
|
+
for prefViewerStr in viewerNames:
|
393
|
+
try:
|
394
|
+
viewerModule, viewerClassName = prefViewerStr.rsplit('.', 1)
|
395
|
+
prefViewer = cls.importFromPlugin(viewerModule,
|
396
|
+
viewerClassName,
|
397
|
+
doRaise=True)
|
398
|
+
viewers.append(prefViewer)
|
399
|
+
except Exception as e:
|
400
|
+
logger.error("Couldn't load \"%s\" as preferred viewer for %s.\n"
|
401
|
+
"There might be a typo in your VIEWERS variable "
|
402
|
+
"or an error in the viewer's plugin installation"
|
403
|
+
% (prefViewerStr, className), exc_info=e)
|
404
|
+
return viewers
|
405
|
+
|
406
|
+
@classmethod
|
407
|
+
def findViewers(cls, className, environment):
|
408
|
+
""" Find the available viewers in this Domain for this class. """
|
409
|
+
viewers = []
|
410
|
+
try:
|
411
|
+
clazz = cls.findClass(className)
|
412
|
+
baseClasses = clazz.mro()
|
413
|
+
preferredViewers = cls.getPreferredViewers(className)
|
414
|
+
preferedFlag = 0
|
415
|
+
|
416
|
+
for viewer in cls.getViewers().values():
|
417
|
+
viewerAdded=False
|
418
|
+
if environment in viewer._environments:
|
419
|
+
for t in viewer._targets:
|
420
|
+
if t in baseClasses:
|
421
|
+
for prefViewer in preferredViewers:
|
422
|
+
if viewer is prefViewer:
|
423
|
+
viewers.insert(0, viewer)
|
424
|
+
viewerAdded=True
|
425
|
+
preferedFlag = 1
|
426
|
+
break
|
427
|
+
else:
|
428
|
+
if t == clazz:
|
429
|
+
viewers.insert(preferedFlag, viewer)
|
430
|
+
viewerAdded = True
|
431
|
+
else:
|
432
|
+
viewers.append(viewer)
|
433
|
+
viewerAdded = True
|
434
|
+
break
|
435
|
+
|
436
|
+
if viewerAdded:
|
437
|
+
break
|
438
|
+
|
439
|
+
except Exception as e:
|
440
|
+
# Catch if there is a missing plugin, we will get Legacy which triggers and Exception
|
441
|
+
pass
|
442
|
+
|
443
|
+
return viewers
|
444
|
+
|
445
|
+
@classmethod
|
446
|
+
def findWizards(cls, protocol, environment):
|
447
|
+
"""
|
448
|
+
Find available wizards for this class, in this Domain.
|
449
|
+
|
450
|
+
:param protocol: Protocol instance for which wizards will be search.
|
451
|
+
:param environment: The environment name for wizards (e.g TKINTER)
|
452
|
+
|
453
|
+
:return A dict with the paramName and wizards for the protocol passed."""
|
454
|
+
return cls.__findWizardsFromDict(protocol, environment,
|
455
|
+
cls.getWizards())
|
456
|
+
|
457
|
+
@classmethod
|
458
|
+
def printInfo(cls):
|
459
|
+
""" Simple function (mainly for debugging) that prints basic
|
460
|
+
information about this Domain. """
|
461
|
+
logger.info("Domain: %s" % cls._name)
|
462
|
+
logger.info(" objects: %s" % len(cls._objects))
|
463
|
+
logger.info(" protocols: %s" % len(cls._protocols))
|
464
|
+
logger.info(" viewers: %s" % len(cls._viewers))
|
465
|
+
logger.info(" wizards: %s" % len(cls._wizards))
|
466
|
+
|
467
|
+
# ---------- Private methods of Domain class ------------------------------
|
468
|
+
@staticmethod
|
469
|
+
def __pluginNotFound(plugName, errorMsg='', doRaise=False):
|
470
|
+
""" Prints or raise (depending on the doRaise) telling why it is failing
|
471
|
+
"""
|
472
|
+
hint = (" Check the plugin manager (Configuration->Plugins in "
|
473
|
+
"Scipion manager window) \n")
|
474
|
+
# the str casting is to work with Exceptions as errorMsg
|
475
|
+
if 'No module named %s' % plugName in str(errorMsg):
|
476
|
+
msgStr = " > %s plugin not found. %s" % (plugName, errorMsg)
|
477
|
+
hint += " or use 'scipion installp --help' in the command line "
|
478
|
+
hint += "to install it."
|
479
|
+
else:
|
480
|
+
msgStr = " > error when importing from %s: %s" % (plugName, errorMsg)
|
481
|
+
if errorMsg != '': # if empty we know nothing...
|
482
|
+
hint += (" or use 'scipion installp --help --checkUpdates' "
|
483
|
+
"in the command line to check for upgrades,\n "
|
484
|
+
"it could be a versions compatibility issue.")
|
485
|
+
|
486
|
+
stackList = traceback.extract_stack()
|
487
|
+
if len(stackList) > 3:
|
488
|
+
callIdx = -3 # We use the most probable index as default
|
489
|
+
for idx, stackLine in enumerate(stackList):
|
490
|
+
if stackLine[0].endswith('/unittest/loader.py'):
|
491
|
+
callIdx = idx + 1
|
492
|
+
else:
|
493
|
+
callIdx = 0
|
494
|
+
|
495
|
+
callBy = stackList[callIdx][0]
|
496
|
+
if callBy.endswith('pyworkflow/plugin.py'):
|
497
|
+
# This special case is to know why is failing and not where is called
|
498
|
+
# because we know that we call all plugins.protocols at the beginning
|
499
|
+
calling = traceback.format_exc().split('\n')[-4]
|
500
|
+
else:
|
501
|
+
line = stackList[callIdx][1]
|
502
|
+
calling = " Called by %s, line %s" % (callBy, line)
|
503
|
+
|
504
|
+
raiseMsg = "%s\n %s\n%s\n" % (msgStr, calling, hint)
|
505
|
+
if doRaise:
|
506
|
+
raise Exception("\n\n" + raiseMsg)
|
507
|
+
else:
|
508
|
+
logger.info(raiseMsg)
|
509
|
+
|
510
|
+
@staticmethod
|
511
|
+
def __getSubmodule(name, subname):
|
512
|
+
try:
|
513
|
+
completeModuleText = '%s.%s' % (name, subname)
|
514
|
+
if pwutils.isModuleAFolder(name):
|
515
|
+
return importlib.import_module(completeModuleText)
|
516
|
+
except Exception as e:
|
517
|
+
msg = str(e)
|
518
|
+
# FIXME: The following is a quick and dirty way to filter
|
519
|
+
# when the submodule is not present
|
520
|
+
if msg != 'No module named \'%s\'' % completeModuleText:
|
521
|
+
Domain.__pluginNotFound(completeModuleText, msg)
|
522
|
+
return None
|
523
|
+
|
524
|
+
@classmethod
|
525
|
+
def __isPlugin(cls, m):
|
526
|
+
""" Return True if the module is a Scipion plugin. """
|
527
|
+
return m.__name__ in cls._plugins
|
528
|
+
|
529
|
+
@staticmethod
|
530
|
+
def __findWizardsFromDict(protocol, environment, wizDict):
|
531
|
+
wizards = {}
|
532
|
+
baseClasses = [c.__name__ for c in protocol.getClass().mro()]
|
533
|
+
|
534
|
+
for wiz in wizDict.values():
|
535
|
+
if environment in wiz._environments:
|
536
|
+
for c, params in wiz._targets:
|
537
|
+
if c.__name__ in baseClasses:
|
538
|
+
for p in params:
|
539
|
+
wizards[p] = wiz
|
540
|
+
return wizards
|
541
|
+
|
542
|
+
|
543
|
+
class Plugin:
|
544
|
+
__metaclass__ = ABCMeta
|
545
|
+
|
546
|
+
_vars = {}
|
547
|
+
_homeVar = '' # Change in subclasses to define the "home" variable
|
548
|
+
_pathVars = []
|
549
|
+
_supportedVersions = []
|
550
|
+
_url = "" # For the plugin
|
551
|
+
_condaActivationCmd = None
|
552
|
+
_tmpName = None # This would be temporary. It will hold the plugin name during the call to defineVariables
|
553
|
+
|
554
|
+
def __init__(self):
|
555
|
+
self._path = None
|
556
|
+
self._inDevelMode = None
|
557
|
+
self._name = None
|
558
|
+
|
559
|
+
@classmethod
|
560
|
+
def _defineVar(cls, varName, defaultValue, description="Missing", var_type:VarTypes=None):
|
561
|
+
""" Internal method to define variables trying to get it from the environment first. """
|
562
|
+
cls._addVar(varName, os.environ.get(varName, defaultValue), default=defaultValue, description=description, var_type=var_type)
|
563
|
+
|
564
|
+
@classmethod
|
565
|
+
def _addVar(cls, varName, value, default=None, description="Missing", var_type:VarTypes=None):
|
566
|
+
""" Adds a variable to the local variable dictionary directly. Avoiding the environment"""
|
567
|
+
|
568
|
+
var = Variable(varName, description, cls._tmpName, value, default, var_type=var_type)
|
569
|
+
VariablesRegistry.register(var)
|
570
|
+
cls._vars[varName] = value
|
571
|
+
|
572
|
+
@classmethod
|
573
|
+
@abstractmethod
|
574
|
+
def getEnviron(cls):
|
575
|
+
""" Set up the environment variables needed to launch programs. """
|
576
|
+
pass
|
577
|
+
|
578
|
+
@classmethod
|
579
|
+
def getCondaActivationCmd(cls):
|
580
|
+
""" Returns the conda activation command with && at the end if defined otherwise empty"""
|
581
|
+
if cls._condaActivationCmd is None:
|
582
|
+
condaActivationCmd = pw.Config.CONDA_ACTIVATION_CMD
|
583
|
+
if not condaActivationCmd:
|
584
|
+
logger.info("WARNING!!_condaActivationCmd: %s variable not defined. "
|
585
|
+
"Relying on conda being in the PATH" % CONDA_ACTIVATION_CMD_VAR)
|
586
|
+
elif condaActivationCmd[-1] not in [";", "&"]:
|
587
|
+
condaActivationCmd += "&&"
|
588
|
+
|
589
|
+
cls._condaActivationCmd = condaActivationCmd
|
590
|
+
|
591
|
+
return cls._condaActivationCmd
|
592
|
+
|
593
|
+
@abstractmethod
|
594
|
+
def _defineVariables(cls):
|
595
|
+
""" Method to define variables and their default values.
|
596
|
+
It will use the method _defineVar that will take a variable value
|
597
|
+
from the environment or from an optional default value.
|
598
|
+
|
599
|
+
This method is not supposed to be called from client code,
|
600
|
+
except from the Domain class when registering a Plugin.
|
601
|
+
"""
|
602
|
+
pass
|
603
|
+
|
604
|
+
@classmethod
|
605
|
+
@abstractmethod
|
606
|
+
def defineBinaries(cls, env):
|
607
|
+
""" Define required binaries in the given Environment. """
|
608
|
+
pass
|
609
|
+
|
610
|
+
@classmethod
|
611
|
+
def getVar(cls, varName, defaultValue=None):
|
612
|
+
""" Return the value of a given variable. """
|
613
|
+
return cls._vars.get(varName, defaultValue)
|
614
|
+
|
615
|
+
@classmethod
|
616
|
+
def getVars(cls):
|
617
|
+
""" Return the value of a given variable. """
|
618
|
+
return cls._vars
|
619
|
+
|
620
|
+
@classmethod
|
621
|
+
def getHome(cls, *paths):
|
622
|
+
""" Return a path from the "home" of the package
|
623
|
+
if the _homeVar is defined in the plugin. """
|
624
|
+
home = cls.getVar(cls._homeVar)
|
625
|
+
return os.path.join(home, *paths) if home else ''
|
626
|
+
|
627
|
+
@classmethod
|
628
|
+
def getSupportedVersions(cls):
|
629
|
+
""" Return the list of supported binary versions. """
|
630
|
+
return cls._supportedVersions
|
631
|
+
|
632
|
+
@classmethod
|
633
|
+
def getActiveVersion(cls, home=None, versions=None):
|
634
|
+
"""
|
635
|
+
Returns the version of the binaries that are currently active.
|
636
|
+
In the current implementation it will be inferred from the *_HOME
|
637
|
+
variable, so it should contain the version number in it. """
|
638
|
+
|
639
|
+
# FIXME: (JMRT) Use the basename might alleviate the issue with matching
|
640
|
+
# the binaries version, but we might consider to find a better solution
|
641
|
+
home = os.path.basename(home or cls.getHome())
|
642
|
+
versions = versions or cls.getSupportedVersions()
|
643
|
+
|
644
|
+
for v in versions:
|
645
|
+
if v in home:
|
646
|
+
return v
|
647
|
+
|
648
|
+
return ''
|
649
|
+
|
650
|
+
@classmethod
|
651
|
+
def validateInstallation(cls):
|
652
|
+
"""
|
653
|
+
Check if the binaries are properly installed and if not, return
|
654
|
+
a list with the error messages.
|
655
|
+
|
656
|
+
The default implementation will check if the _pathVars exists.
|
657
|
+
"""
|
658
|
+
try:
|
659
|
+
missing = ["%s: %s" % (var, cls.getVar(var))
|
660
|
+
for var in cls._pathVars if not os.path.exists(cls.getVar(var))]
|
661
|
+
|
662
|
+
return (["Missing paths: the variables below point to non existing paths."]
|
663
|
+
+ missing + [
|
664
|
+
"Either install the software ( %s )" % DOCSITEURLS.PLUGIN_MANAGER,
|
665
|
+
"or edit the config file ( %s )" % DOCSITEURLS.CONFIG]) if missing else []
|
666
|
+
except Exception as e:
|
667
|
+
return ["validateInstallation fails: %s" % e]
|
668
|
+
|
669
|
+
@classmethod
|
670
|
+
def getUrl(cls, protClass=None):
|
671
|
+
""" Url for the plugin to point users to it"""
|
672
|
+
return cls._url
|
673
|
+
|
674
|
+
# -------------- Instance methods ----------------
|
675
|
+
def getName(self):
|
676
|
+
if self._name is None:
|
677
|
+
self._name= self.__class__.__module__
|
678
|
+
|
679
|
+
return self._name
|
680
|
+
|
681
|
+
def getPluginDir(self):
|
682
|
+
return self.getPath()
|
683
|
+
|
684
|
+
def getPluginTemplateDir(self):
|
685
|
+
return os.path.join(self.getPath(), 'templates')
|
686
|
+
|
687
|
+
|
688
|
+
def getTemplates(self):
|
689
|
+
""" Get the plugin templates from the templates directory.
|
690
|
+
If more than one template is found or passed, a dialog is raised
|
691
|
+
to choose one.
|
692
|
+
"""
|
693
|
+
tempList = []
|
694
|
+
pluginName = self.getName()
|
695
|
+
tDir = self.getPluginTemplateDir()
|
696
|
+
if os.path.exists(tDir):
|
697
|
+
for file in glob.glob1(tDir, "*" + SCIPION_JSON_TEMPLATES):
|
698
|
+
t = LocalTemplate(pluginName, os.path.join(tDir, file))
|
699
|
+
tempList.append(t)
|
700
|
+
|
701
|
+
return tempList
|
702
|
+
|
703
|
+
def getPath(self):
|
704
|
+
if self._path is None:
|
705
|
+
self._path = sys.modules[self.__class__.__module__].__path__[0]
|
706
|
+
|
707
|
+
return self._path
|
708
|
+
def inDevelMode(self)-> bool:
|
709
|
+
""" Returns true if code is not in python's site-packages folder"""
|
710
|
+
if self._inDevelMode is None:
|
711
|
+
self._inDevelMode = pwutils.getPythonPackagesFolder() not in self.getPath()
|
712
|
+
return self._inDevelMode
|