source: orange/Orange/OrangeCanvas/orngRegistry.py @ 10838:3e65e0fec526

Revision 10838:3e65e0fec526, 12.9 KB checked in by mitar, 2 years ago (diff)

Support also add-ons in Python eggs.

Line 
1# Author: Gregor Leban (gregor.leban@fri.uni-lj.si)
2#
3
4import os, sys, re, glob, stat
5import pkg_resources
6from orngSignalManager import OutputSignal, InputSignal, resolveSignal
7from PyQt4.QtCore import *
8from PyQt4.QtGui import *
9import widgetParser
10
11orangeDir = os.path.split(os.path.split(os.path.abspath(__file__))[0])[0]
12if not orangeDir in sys.path:
13    sys.path.append(orangeDir)
14
15import orngEnviron, Orange.utils.addons
16from Orange.utils import addons
17
18WIDGETS_ENTRY_POINT = 'orange.widgets'
19
20class WidgetDescription(object):
21    def __init__(self, **attrs):
22        self.__dict__.update(attrs)
23
24    def docDir(self):
25        if not self.addOn:  # A built-in widget
26            dir, widgetDir = os.path.realpath(self.directory), os.path.realpath(orngEnviron.widgetDir)
27            subDir = os.path.relpath(dir, widgetDir) if "relpath" in os.path.__dict__ else dir.replace(widgetDir, "")
28            return os.path.join(orngEnviron.orangeDocDir, "widgets", subDir)
29        else:  # An add-on widget
30            addOnDocDir = self.addOn.directory_documentation()
31            return os.path.join(addOnDocDir, "widgets")
32
33
34class WidgetCategory(dict):
35    def __init__(self, name, widgets=None):
36        if widgets:
37            self.update(widgets)
38        self.name = name
39   
40def readCategories(silent=False):
41    try:
42        from Orange.version import version as orange_version
43    except ImportError:
44        # Orange.version module is writen by setup.py, what if orange was build
45        # using make
46        orange_version = "???"
47    # Add orange version to the cache version (because cache contains names
48    # of types inside the Orange hierarchy, if that changes the cache should be
49    # invalidated)
50    currentCacheVersion = (3, orange_version)
51   
52    global widgetsWithError, widgetsWithErrorPrototypes
53    widgetDirName = os.path.realpath(orngEnviron.directoryNames["widgetDir"])
54    canvasSettingsDir = os.path.realpath(orngEnviron.directoryNames["canvasSettingsDir"])
55    cacheFilename = os.path.join(canvasSettingsDir, "cachedWidgetDescriptions.pickle")
56
57    try:
58        import cPickle
59        cacheFile = file(cacheFilename, "rb")
60        cats = cPickle.load(cacheFile)
61        try:
62            version = cPickle.load(cacheFile)
63        except EOFError:
64            version = 0
65        if version == currentCacheVersion:
66            cachedWidgetDescriptions = dict([(w.fullName, w) for cat in cats.values() for w in cat.values()])
67        else:
68            cachedWidgetDescriptions = {}
69    except Exception:
70        cachedWidgetDescriptions = {} 
71
72    directories = [] # tuples (defaultCategory, dirName, plugin, isPrototype)
73    for dirName in os.listdir(widgetDirName):
74        directory = os.path.join(widgetDirName, dirName)
75        if os.path.isdir(directory):
76            directories.append((None, directory, None, "prototypes" in dirName.lower(), None))
77           
78    # read list of add-ons
79    for addOn in Orange.utils.addons.installed_addons.values() + Orange.utils.addons.registered_addons:
80        addOnWidgetsDir = os.path.join(addOn.directory, "widgets")
81        if os.path.isdir(addOnWidgetsDir):
82            directories.append((addOn.name, addOnWidgetsDir, addOn, False, None))
83        addOnWidgetsPrototypesDir = os.path.join(addOnWidgetsDir, "prototypes")
84        if os.path.isdir(addOnWidgetsPrototypesDir):
85            directories.append((None, addOnWidgetsPrototypesDir, addOn, True, None))
86
87    # New-type add-ons
88    for entry_point in pkg_resources.iter_entry_points(WIDGETS_ENTRY_POINT):
89        try:
90            module = entry_point.load()
91            if hasattr(module, '__path__'):
92                # It is a package
93                addOn = addons.OrangeAddOn()
94                addOn.name = entry_point.name
95                addOn.directory = module.__path__[0] # This is invalid and useless as documentation is not there, but to set it to something
96                directories.append((entry_point.name, module.__path__[0], addOn, False, module))
97            else:
98                # It is a module
99                # TODO: Implement loading of widget modules
100                # (This should be default way to load widgets, not parsing them as files, or traversing directories, just modules and packages (which load modules))
101                pass
102        except ImportError, err:
103            print "While loading, importing widgets '%s' failed: %s" % (entry_point.name, err)
104
105    categories = {}     
106    for defCat, dirName, addOn, isPrototype, module in directories:
107        widgets = readWidgets(dirName, cachedWidgetDescriptions, isPrototype, silent=silent, addOn=addOn, defaultCategory=defCat, module=module)
108        for (wName, wInfo) in widgets:
109            catName = wInfo.category
110            if not catName in categories:
111                categories[catName] = WidgetCategory(catName)
112            if wName in categories[catName]:
113                print "Warning! A widget with duplicated name '%s' in category '%s' has been found! It will _not_ be available in the Canvas." % (wName, catName)
114            else:
115                categories[catName][wName] = wInfo
116
117    cacheFile = file(cacheFilename, "wb")
118    cPickle.dump(categories, cacheFile)
119    cPickle.dump(currentCacheVersion, cacheFile)
120    if splashWindow:
121        splashWindow.hide()
122
123    if not silent:
124        if widgetsWithError != []:
125            print "The following widgets could not be imported and will not be available: " + ", ".join(set(widgetsWithError)) + "."
126        if widgetsWithErrorPrototypes != []:
127            print "The following prototype widgets could not be imported and will not be available: " + ", ".join(set(widgetsWithErrorPrototypes)) + "."
128
129    return categories
130
131
132hasErrors = False
133splashWindow = None
134widgetsWithError = []
135widgetsWithErrorPrototypes = []
136
137def readWidgets(directory, cachedWidgetDescriptions, prototype=False, silent=False, addOn=None, defaultCategory=None, module=None):
138    import sys, imp
139    global hasErrors, splashWindow, widgetsWithError, widgetsWithErrorPrototypes
140   
141    widgets = []
142
143    if not defaultCategory:
144        predir, defaultCategory = os.path.split(directory.strip(os.path.sep).strip(os.path.altsep))
145        if defaultCategory == "widgets":
146            defaultCategory = os.path.split(predir.strip(os.path.sep).strip(os.path.altsep))[1]
147   
148    if defaultCategory.lower() == "prototypes" or prototype:
149        defaultCategory = "Prototypes"
150   
151    if module:
152        files = [f for f in pkg_resources.resource_listdir(module.__name__, '') if f.endswith('.py')]
153    else:
154        files = glob.iglob(os.path.join(directory, "*.py"))
155
156    for filename in files:
157        if module:
158            if pkg_resources.resource_isdir(module.__name__, filename):
159                continue
160        else:
161            if os.path.isdir(filename):
162                continue
163       
164        if module:
165            if getattr(module, '__loader__', None):
166                datetime = str(os.stat(module.__loader__.archive)[stat.ST_MTIME])
167            else:
168                datetime = str(os.stat(pkg_resources.resource_filename(module.__name__, filename))[stat.ST_MTIME])
169        else:
170            datetime = str(os.stat(filename)[stat.ST_MTIME])
171        cachedDescription = cachedWidgetDescriptions.get(filename, None)
172        if cachedDescription and cachedDescription.time == datetime and hasattr(cachedDescription, "inputClasses"):
173            widgets.append((cachedDescription.name, cachedDescription))
174            continue
175       
176        if module:
177            data = pkg_resources.resource_string(module.__name__, filename)
178        else:
179            data = file(filename).read()
180        try:
181            meta = widgetParser.WidgetMetaData(data, defaultCategory, enforceDefaultCategory=prototype)
182        except:   # Probably not an Orange widget module.
183            continue
184
185        widgetPrototype = meta.prototype == "1" or meta.prototype.lower().strip() == "true" or prototype
186        if widgetPrototype:
187            meta.category = "Prototypes"
188
189        dirname, fname = os.path.split(filename)
190        widgname = os.path.splitext(fname)[0]
191        try:
192            if not splashWindow:
193                import orngEnviron
194                logo = QPixmap(os.path.join(orngEnviron.directoryNames["canvasDir"], "icons", "splash.png"))
195                splashWindow = QSplashScreen(logo, Qt.WindowStaysOnTopHint)
196                splashWindow.setMask(logo.mask())
197                splashWindow.show()
198               
199            splashWindow.showMessage("Registering widget %s" % meta.name, Qt.AlignHCenter + Qt.AlignBottom)
200            qApp.processEvents()
201           
202            # We import modules using imp.load_source to avoid storing them in sys.modules,
203            # but we need to append the path to sys.path in case the module would want to load
204            # something
205            if dirname:
206                dirnameInPath = dirname in sys.path
207                if not dirnameInPath:
208                    sys.path.append(dirname)
209            if module:
210                # TODO: We could optimize this probably with loading the module in a way which would not need filename directly
211                wmod = imp.load_source("%s.%s" % (module.__name__, widgname), pkg_resources.resource_filename(module.__name__, filename))
212            else:
213                wmod = imp.load_source(widgname, filename)
214            if dirname and not dirnameInPath and dirname in sys.path: # I have no idea, why we need this, but it seems to disappear sometimes?!
215                sys.path.remove(dirname)
216            widgClass = wmod.__dict__[widgname]
217
218            # Evaluate the input/output list (all tuple items are strings)
219            inputs = eval(meta.inputList)
220            outputs = eval(meta.outputList)
221
222            inputs = [InputSignal(*input) for input in inputs]
223            outputs = [OutputSignal(*output) for output in outputs]
224
225            # Resolve signal type names into concrete type instances
226            inputs = [resolveSignal(input, globals=wmod.__dict__)
227                      for input in inputs]
228            outputs = [resolveSignal(output, globals=wmod.__dict__)
229                      for output in outputs]
230
231            inputClasses = set([s.type.__name__ for s in inputs])
232            outputClasses = set([klass.__name__ for s in outputs
233                                 for klass in s.type.mro()])
234
235            # Convert all signal types back into qualified names.
236            # This is to prevent any possible import problems when cached
237            # descriptions are unpickled (the relevant code using this lists
238            # should be able to handle missing types better).
239            for s in inputs + outputs:
240                s.type = "%s.%s" % (s.type.__module__, s.type.__name__)
241
242            widgetInfo = WidgetDescription(
243                             name =meta.name,
244                             time = datetime,
245                             fileName = widgname,
246                             module = module.__name__ if module else None,
247                             fullName = wmod.__file__,
248                             directory = directory,
249                             addOn = addOn,
250                             inputList = meta.inputList, outputList = meta.outputList,
251                             inputClasses = inputClasses, outputClasses = outputClasses,
252                             tags=meta.tags,
253                             inputs=inputs,
254                             outputs=outputs,
255                             )
256
257            for attr in ["contact", "icon", "priority", "description", "category"]:
258                setattr(widgetInfo, attr, getattr(meta, attr))
259
260            # build the tooltip
261            if len(widgetInfo.inputs) == 0:
262                formatedInList = "<b>Inputs:</b><br> &nbsp;&nbsp; None<br>"
263            else:
264                formatedInList = "<b>Inputs:</b><br>"
265                for signal in widgetInfo.inputs:
266                    formatedInList += " &nbsp;&nbsp; - " + signal.name + " (" + signal.type + ")<br>"
267
268            if len(widgetInfo.outputs) == 0:
269                formatedOutList = "<b>Outputs:</b><br> &nbsp; &nbsp; None<br>"
270            else:
271                formatedOutList = "<b>Outputs:</b><br>"
272                for signal in widgetInfo.outputs:
273                    formatedOutList += " &nbsp; &nbsp; - " + signal.name + " (" + signal.type + ")<br>"
274
275            addOnName = "" if not widgetInfo.addOn else " (from add-on %s)" % widgetInfo.addOn.name
276   
277            widgetInfo.tooltipText = "<b><b>&nbsp;%s</b></b>%s<hr><b>Description:</b><br>&nbsp;&nbsp;%s<hr>%s<hr>%s" % (meta.name, addOnName, widgetInfo.description, formatedInList[:-4], formatedOutList[:-4]) 
278            widgets.append((meta.name, widgetInfo))
279        except Exception, msg:
280            if not hasErrors and not silent:
281                print "There were problems importing the following widgets:"
282                hasErrors = True
283            if not silent:
284                print "   %s: %s" % (widgname, msg)
285
286            if not widgetPrototype:
287                widgetsWithError.append(widgname)
288            else:
289                widgetsWithErrorPrototypes.append(widgname)
290       
291    return widgets
Note: See TracBrowser for help on using the repository browser.