Changeset 11018:b7cbf2b86522 in orange


Ignore:
Timestamp:
11/13/12 20:05:23 (17 months ago)
Author:
Matija Polajnar <matija.polajnar@…>
Branch:
default
Message:

Rewrite the add-on support modules and GUI to support the new properly packed add-ons, published on PyPI.

Location:
Orange
Files:
4 edited

Legend:

Unmodified
Added
Removed
  • Orange/OrangeCanvas/orngCanvas.pyw

    r10903 r11018  
    848848        if t - lastRefresh > 7*24*3600: 
    849849            if QMessageBox.question(self, "Refresh", 
    850                                     "List of add-ons in repositories has %s. Do you want to %s the lists now?" % 
     850                                    "List of add-ons in repository has %s. Do you want to %s the list now?" % 
    851851                                    (("not yet been loaded" if lastRefresh==0 else "not been refreshed for more than a week"), 
    852852                                     ("download" if lastRefresh==0 else "reload")), 
     
    854854                                     QMessageBox.No | QMessageBox.Escape) == QMessageBox.Yes: 
    855855                 
    856                 anyFailed = False 
    857                 anyDone = False 
    858                 for r in Orange.utils.addons.available_repositories: 
    859                     #TODO: # Should show some progress (and enable cancellation) 
    860                     try: 
    861                         if r.refreshdata(force=True): 
    862                             anyDone = True 
    863                         else: 
    864                             anyFailed = True 
    865                     except Exception, e: 
    866                         anyFailed = True 
    867                         print "Unable to refresh repository %s! Error: %s" % (r.name, e) 
    868                  
    869                 if anyDone: 
    870                     self.settings["lastAddonsRefresh"] = t 
    871                 if anyFailed: 
    872                     QMessageBox.warning(self,'Download Failed', "Download of add-on list has failed for at least one repostitory.") 
    873          
     856                #TODO: # Should show some progress (and enable cancellation) 
     857                try: 
     858                    Orange.utils.addons.refresh_available_addons() 
     859                    self.settings["lastAddonsRefresh"] = time.time() 
     860                except Exception, e: 
     861                    import traceback 
     862                    traceback.print_exc() 
     863                    QMessageBox.warning(self,'Download Failed', "Download of add-on list has failed.") 
     864 
    874865        dlg = orngDlgs.AddOnManagerDialog(self, self) 
    875866        if dlg.exec_() == QDialog.Accepted: 
    876             for (id, addOn) in dlg.addOnsToRemove.items(): 
     867            add, remove, upgrade = dlg.to_install(), dlg.to_remove(), dlg.to_upgrade 
     868            for name in upgrade: 
    877869                try: 
    878                     addOn.uninstall(refresh=False) 
    879                     if id in dlg.addOnsToAdd.items(): 
    880                         Orange.utils.addons.install_addon_from_repo(dlg.addOnsToAdd[id], global_install=False, refresh=False) 
    881                         del dlg.addOnsToAdd[id] 
     870                    Orange.utils.addons.upgrade(name) 
    882871                except Exception, e: 
    883                     print "Problem %s add-on %s: %s" % ("upgrading" if id in dlg.addOnsToAdd else "removing", addOn.name, e) 
    884             for (id, addOn) in dlg.addOnsToAdd.items(): 
    885                 if id.startswith("registered:"): 
    886                     try: 
    887                         Orange.utils.addons.register_addon(addOn.name, addOn.directory, refresh=False, systemwide=False) 
    888                     except Exception, e: 
    889                         print "Problem registering add-on %s: %s" % (addOn.name, e) 
    890                 else: 
    891                     try: 
    892                         Orange.utils.addons.install_addon_from_repo(dlg.addOnsToAdd[id], global_install=False, refresh=False) 
    893                     except Exception, e: 
    894                         print "Problem installing add-on %s: %s" % (addOn.name, e) 
    895             if len(dlg.addOnsToAdd)+len(dlg.addOnsToRemove)>0: 
    896                 Orange.utils.addons.refresh_addons(reload_path=True) 
    897                  
     872                    print "Problem upgrading add-on %s: %s" % (name, e) 
     873            for name in remove: 
     874                try: 
     875                    Orange.utils.addons.uninstall(name) 
     876                except Exception, e: 
     877                    print "Problem uninstalling add-on %s: %s" % (name, e) 
     878            for name in add: 
     879                try: 
     880                    Orange.utils.addons.install(name) 
     881                except Exception, e: 
     882                    print "Problem installing add-on %s: %s" % (name, e) 
     883 
    898884    def menuItemShowStatusBar(self): 
    899885        state = self.showStatusBarAction.isChecked() 
  • Orange/OrangeCanvas/orngDlgs.py

    r10778 r11018  
    675675 
    676676class AddOnManagerSummary(QDialog): 
    677     def __init__(self, add, remove, *args): 
     677    def __init__(self, add, remove, upgrade, *args): 
    678678        apply(QDialog.__init__,(self,) + args) 
    679679        self.setWindowTitle("Pending Actions") 
     
    697697                        lambda docSize: self.updateMinSize(docSize)) 
    698698        actions = [] 
    699         for addOnId in add: 
    700             if addOnId in remove: 
    701                 actions.append("Upgrade %s." % add[addOnId].name) 
    702             elif addOnId.startswith("registered:"): 
    703                 actions.append("Register %s." % add[addOnId].name) 
    704             else: 
    705                 actions.append("Install %s." % add[addOnId].name) 
    706         for addOnId in remove: 
    707             if not addOnId in add: 
    708                 if addOnId.startswith("registered:"): 
    709                     actions.append("Unregister %s." % remove[addOnId].name) 
    710                 else: 
    711                     actions.append("Remove %s." % remove[addOnId].name) 
    712         actions.sort() 
     699        for ao in add: 
     700            actions.append("Install %s." % ao) 
     701        for ao in remove: 
     702            actions.append("Remove %s." % ao) 
     703        for ao in upgrade: 
     704            actions.append("Upgrade %s." % ao) 
    713705        memo.setText("\n".join(actions)) 
    714706         
     
    726718        self.memo.setMinimumHeight(min(300, documentSize.height() + 2 * self.memo.frameWidth())) 
    727719 
    728  
    729 class AddOnRepositoryData(QDialog): 
    730     def __init__(self, name="", url="", *args): 
    731         apply(QDialog.__init__,(self,) + args) 
    732         self.setWindowTitle("Add-on Repository") 
    733         self.topLayout = QVBoxLayout(self) 
    734         self.topLayout.setSpacing(0) 
    735          
    736         self.name = name 
    737         self.url = url      
    738          
    739         eName = OWGUI.lineEdit(self, self, "name", "Display name:", orientation="horizontal", controlWidth=150) 
    740         eName.parent().layout().addStretch(1) 
    741         eURL = OWGUI.lineEdit(self, self, "url", "URL:", orientation="horizontal", controlWidth=250) 
    742         eURL.parent().layout().addStretch(1) 
    743         self.layout().addSpacing(15) 
    744         hbox = OWGUI.widgetBox(self, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)) 
    745         hbox.layout().addStretch(1) 
    746         self.okButton = OWGUI.button(hbox, self, "OK", callback = self.accept) 
    747         self.cancelButton = OWGUI.button(hbox, self, "Cancel", callback = self.reject) 
    748         self.okButton.setDefault(True) 
    749          
    750     def accept(self): 
    751         if self.name.strip() == "": 
    752             QMessageBox.warning(self, "Incorrect Input", "Name cannot be empty") 
    753             return 
    754         if self.url.strip() == "": 
    755             QMessageBox.warning(self, "Incorrect Input", "URL cannot be empty") 
    756             return 
    757         QDialog.accept(self) 
    758          
    759          
    760720class AddOnManagerDialog(QDialog): 
    761721    def __init__(self, canvasDlg, *args): 
     
    773733         
    774734        self.groupByRepo = True 
    775         self.sortInstalledFirst = True 
    776         self.sortSingleLast = True 
    777735        self.searchStr = "" 
     736        self.to_upgrade = set() 
    778737 
    779738        searchBox = OWGUI.widgetBox(mainBox, orientation = "horizontal", sizePolicy = QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)) 
    780739 
    781         self.viewBox = viewBox = OWGUI.SmallWidgetLabel(searchBox, pixmap = 1, box = "Grouping and Order", tooltip = "Adjust the order of add-ons in the list") 
    782         cGroupByRepo        = OWGUI.checkBox(viewBox.widget, self, "groupByRepo", "&Group by repository", callback = self.refreshView) 
    783         cSortInstalledFirst = OWGUI.checkBox(viewBox.widget, self, "sortInstalledFirst", "&Installed first", callback = self.refreshView) 
    784         cSortSingleLast     = OWGUI.checkBox(viewBox.widget, self, "sortSingleLast", "&Single widgets last", callback = self.refreshView) 
    785  
    786740        self.eSearch = self.lineEditSearch(searchBox, self, "searchStr", None, 0, tooltip = "Type in to filter (search) add-ons.", callbackOnType=True, callback=self.searchCallback) 
    787741         
    788742        # Repository & Add-on tree 
    789743         
    790         repos = OWGUI.widgetBox(mainBox, "Add-ons", orientation = "horizontal", sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) 
     744        repos = OWGUI.widgetBox(mainBox, "Add-ons", orientation = "horizontal", sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)) 
    791745        repos.layout().setSizeConstraint(QLayout.SetMinimumSize) 
    792         self.tree = tree = QTreeWidget(repos) 
    793         self.tree.setMinimumWidth(200) 
    794         self.tree.repoItems = {} 
    795         tree.header().hide() 
    796         repos.layout().addWidget(tree) 
    797         QObject.connect(tree, SIGNAL("itemChanged(QTreeWidgetItem *, int)"), self.cbToggled) 
    798         QObject.connect(tree, SIGNAL("currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *)"), self.currentItemChanged) 
    799  
    800         self.addOnsToAdd = {} 
    801         self.addOnsToRemove = {} 
     746        self.lst = lst = QListWidget(repos) 
     747        lst.setMinimumWidth(200) 
     748        lst.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)) 
     749        repos.layout().addWidget(lst) 
     750        QObject.connect(lst, SIGNAL("itemChanged(QListWidgetItem *)"), self.cbToggled) 
     751        QObject.connect(lst, SIGNAL("currentItemChanged(QListWidgetItem *, QListWidgetItem *)"), self.currentItemChanged) 
     752 
    802753        import Orange.utils.addons 
    803         self.repositories = [repo.clone() for repo in Orange.utils.addons.available_repositories] 
    804          
     754 
    805755        # Bottom info pane 
    806756         
    807         self.infoPane = infoPane = OWGUI.widgetBox(mainBox, orientation="vertical", sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)) 
     757        self.infoPane = infoPane = OWGUI.widgetBox(mainBox, orientation="vertical", sizePolicy=QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)) 
    808758        infoPane.layout().setSizeConstraint(QLayout.SetMinimumSize) 
    809759 
     
    833783        pInfoBtns = OWGUI.widgetBox(infoPane, orientation="horizontal", sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)) 
    834784        self.webButton = OWGUI.button(pInfoBtns, self, "Open webpage", callback = self.openWebPage) 
     785        self.docButton = OWGUI.button(pInfoBtns, self, "Open documentation", callback = self.openDocsPage) 
    835786        self.listWidgetsButton = OWGUI.button(pInfoBtns, self, "List widgets", callback = self.listWidgets) 
    836787        pInfoBtns.layout().addStretch(1) 
     
    859810        rightPanel = OWGUI.widgetBox(repos, orientation = "vertical", sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)) 
    860811        rightPanel.layout().setSizeConstraint(QLayout.SetMinimumSize) 
    861         self.addRepoButton = OWGUI.button(rightPanel, self, "Add Repository...", callback = self.addRepo) 
    862         self.editRepoButton = OWGUI.button(rightPanel, self, "Edit Repository...", callback = self.editRepo) 
    863         self.delRepoButton = OWGUI.button(rightPanel, self, "Remove Repository", callback = self.delSelectedRepo) 
    864         self.reloadRepoButton = OWGUI.button(rightPanel, self, "Refresh lists", callback = self.reloadRepos) 
     812        self.reloadRepoButton = OWGUI.button(rightPanel, self, "Refresh list", callback = self.reloadRepo) 
    865813        rightPanel.layout().addSpacing(15) 
    866814        self.upgradeAllButton = OWGUI.button(rightPanel, self, "Upgrade All", callback = self.upgradeAll) 
    867         rightPanel.layout().addSpacing(15) 
    868         self.registerButton = OWGUI.button(rightPanel, self, "Register Add-on...", callback = self.registerAddOn) 
    869815        rightPanel.layout().addStretch(1) 
    870816        for btn in rightPanel.children(): 
     
    887833     
    888834    def accept(self): 
     835        self.to_upgrade.difference_update(self.to_remove()) 
    889836        import Orange.utils.addons 
    890         if len(self.addOnsToAdd) + len(self.addOnsToRemove) > 0: 
    891             summary = AddOnManagerSummary(self.addOnsToAdd, self.addOnsToRemove, self) 
     837        add, remove, upgrade = self.to_install(), self.to_remove(), self.to_upgrade 
     838        if len(add) + len(remove) + len(upgrade) > 0: 
     839            summary = AddOnManagerSummary(add, remove, upgrade, self) 
    892840            if summary.exec_() == QDialog.Rejected: 
    893841                return 
    894         Orange.utils.addons.available_repositories = self.repositories 
    895         Orange.utils.addons.save_repositories() 
    896842        QDialog.accept(self) 
    897843         
    898     def addRepo(self): 
    899         dlg = AddOnRepositoryData() 
    900         while dlg.exec_() == QDialog.Accepted: 
    901             import Orange.utils.addons 
    902             try: 
    903                 repo = Orange.utils.addons.OrangeAddOnRepository(dlg.name, dlg.url)   #TODO: This can take some time - inform the user! 
    904                 self.repositories.append(repo) 
    905             except Exception, e: 
    906                 QMessageBox.critical(self, "Error", "Could not add this repository: %s"%e) 
    907                 continue 
    908             break 
    909         self.refreshView() 
    910  
    911     def editRepo(self, repo=None): 
    912         if not repo: 
    913             repo = self.getRepoFromItem(self.tree.currentItem()) 
    914         if not repo: 
    915             return 
    916         dlg = AddOnRepositoryData(name=repo.name, url=repo.url) 
    917         while dlg.exec_() == QDialog.Accepted: 
    918             import Orange.utils.addons 
    919             try: 
    920                 oldname, oldurl = repo.name, repo.url 
    921                 repo.name, repo.url = dlg.name, dlg.url 
    922                 if oldurl != repo.url: 
    923                     repo.refreshdata(force=True)  #TODO: This can take some time - inform the user! 
    924             except Exception, e: 
    925                 repo.name, repo.url = oldname, oldurl 
    926                 QMessageBox.critical(self, "Error", "Could not load repository %s."%e) 
    927                 continue 
    928             break 
    929         self.refreshView() 
    930  
    931     def delSelectedRepo(self): 
    932         repo = self.getRepoFromItem(self.tree.currentItem()) 
    933         if repo==None: 
    934             return 
    935         # Is it a default repository? We cannot delete it! 
     844    def reloadRepo(self): 
     845        # Reload add-on list. 
     846        # TODO: This can take some time - show some progress to user! 
    936847        import Orange.utils.addons 
    937         if repo.__class__ is Orange.utils.addons.OrangeDefaultAddOnRepository: 
    938             return 
    939          
    940         # Are there add-ons selected for installation from this repository? We remove the installation requests. 
    941         for (id, addOn) in self.addOnsToAdd.items(): 
    942             if addOn.repository == repo: 
    943                 del self.addOnsToAdd[id] 
    944          
    945         # Remove the repository and refresh tree. 
    946         self.repositories.remove(repo) 
    947         self.refreshView() 
    948      
    949     def reloadRepos(self): 
    950         # Reload add-on list for all repositories. 
    951         # TODO: This can take some time - show some progress to user! 
    952         for repo in self.repositories: 
    953             try: 
    954                 repo.refreshdata(force=True) 
    955             except Exception, e:  # Maybe gather all exceptions (for all repositories) and show them in the end? 
    956                 QMessageBox.critical(self, "Error", "Could not reload repository '%s': %s." % (repo.name, e)) 
    957         # Were any installation-pending add-ons removed from repositories? 
    958         for (id, addOn) in self.addOnsToAdd.items(): 
    959             if id in addOn.repository.addons: 
    960                 newObject = [version for version in addOn.repository.addons[id] if version.version == addOn.version] 
    961                 if newObject != []: 
    962                     self.addOnsToAdd[id] = newObject[0] 
    963                     continue 
    964             del self.addOnsToAdd[id] 
    965             if id in self.addOnsToRemove:    # If true, it was a request for upgrade, not installation -- do not remove the installed version! 
    966                 del self.addOnsToRemove[id] 
     848        try: 
     849            Orange.utils.addons.refresh_available_addons() 
     850        except Exception, e:  # Maybe gather all exceptions (for all repositories) and show them in the end? 
     851            QMessageBox.critical(self, "Error", "Could not reload repository '%s': %s." % (repo.name, e)) 
    967852        # Finally, refresh the tree on GUI. 
    968853        self.refreshView() 
     
    970855    def upgradeCandidates(self): 
    971856        result = [] 
    972         import orngEnviron, Orange.utils.addons 
    973         for item in self.tree.addOnItems: 
    974             id = item.newest.id 
    975             if id.startswith("registered:"): continue 
    976             installedAo = Orange.utils.addons.installed_addons[id] if id in Orange.utils.addons.installed_addons else None 
    977             installed = installedAo.version if installedAo else None  
    978             selected = self.addOnsToAdd[id].version if id in self.addOnsToAdd else None 
    979             if installed: 
    980                 if installedAo.directory.startswith(orngEnviron.addOnsDirUser): 
    981                     if installed < item.newest.version: 
    982                         if selected: 
    983                             if selected >= item.newest.version: 
    984                                 continue 
    985                         result.append(item.newest) 
     857        import Orange.utils.addons 
     858        for ao in Orange.utils.addons.addons.values(): 
     859            if ao.installed_version and ao.available_version and ao.installed_version != ao.available_version: 
     860                result.append(ao.name) 
    986861        return result 
    987862     
     
    990865            self.upgrade(candidate, refresh=False) 
    991866        self.refreshInfoPane() 
    992         self.enableDisableButtons() 
    993          
    994     def upgrade(self, newAddOn=None, refresh=True): 
    995         if not newAddOn: 
    996             newAddOn = self.getAddOnFromItem(self.tree.currentItem()) 
    997         if not newAddOn: 
    998             return 
    999         import Orange.utils.addons 
    1000         self.addOnsToRemove[newAddOn.id] = Orange.utils.addons.installed_addons[newAddOn.id] 
    1001         self.addOnsToAdd[newAddOn.id] = newAddOn 
     867 
     868    def upgrade(self, name=None, refresh=True): 
     869        if not name: 
     870            name = self.getAddOnIdFromItem(self.lst.currentItem()) 
     871        self.to_upgrade.add(name) 
    1002872        if refresh: 
    1003873            self.refreshInfoPane() 
    1004             self.enableDisableButtons() 
    1005  
    1006     def registerAddOn(self): 
    1007         dir = str(QFileDialog.getExistingDirectory(self, "Select the folder that contains the add-on:")) 
    1008         if dir != "": 
    1009             if os.path.split(dir)[1] == "widgets":     # register a dir above the dir that contains the widget folder 
    1010                 dir = os.path.split(dir)[0] 
    1011             if os.path.exists(os.path.join(dir, "widgets")): 
    1012                 name = os.path.split(dir)[1] 
    1013                 import Orange.utils.addons 
    1014                 id = "registered:"+dir 
    1015                 self.addOnsToAdd[id] = Orange.utils.addons.OrangeRegisteredAddOn(name, dir, systemwide=False) 
    1016                 self.refreshView(id) 
    1017             else: 
    1018                 QMessageBox.information( None, "Information", 'The specified folder does not seem to contain an Orange add-on.', QMessageBox.Ok + QMessageBox.Default) 
    1019874 
    1020875    def openWebPage(self): 
    1021         addOn = self.getAddOnFromItem(self.tree.currentItem()) 
    1022         if not addOn: return 
    1023         if not addOn.homepage: return 
    1024         import webbrowser 
    1025         webbrowser.open(addOn.homepage) 
    1026          
     876        addon = self.getAddOnFromItem(self.lst.currentItem()) 
     877        if addon and addon.homepage: 
     878            import webbrowser 
     879            webbrowser.open(addon.homepage) 
     880 
     881    def openDocsPage(self): 
     882        addon = self.getAddOnFromItem(self.lst.currentItem()) 
     883        if addon and addon.docs_url: 
     884            import webbrowser 
     885            webbrowser.open(addon.docs_url) 
     886 
    1027887    def listWidgets(self): 
    1028         addOn = self.getAddOnFromItem(self.tree.currentItem()) 
     888        addOn = self.getAddOnFromItem(self.lst.currentItem()) 
    1029889        if not addOn: return 
    1030890        import Orange.utils.addons 
     
    1034894         
    1035895         
    1036     def donotUpgrade(self, newAddOn=None): 
    1037         if not newAddOn: 
    1038             newAddOn = self.getAddOnFromItem(self.tree.currentItem()) 
    1039         if not newAddOn: 
    1040             return 
    1041         del self.addOnsToAdd[newAddOn.id] 
    1042         del self.addOnsToRemove[newAddOn.id] 
     896    def donotUpgrade(self): 
     897        id = self.getAddOnIdFromItem(self.lst.currentItem()) 
     898        self.to_upgrade.remove(id) 
    1043899        self.refreshInfoPane() 
    1044          
     900 
     901    def cbToggled(self, item): 
     902        self.refreshInfoPane(item) 
     903 
    1045904    def lineEditSearch(self, *args, **props): 
    1046905        return OWGUI.lineEdit(*args, **props) 
    1047906 
    1048     def cbToggled(self, item, column): 
    1049         # Not a request from an add-on item in tree? 
    1050         if (column != 0) or "disableToggleSignal" not in item.__dict__: 
    1051             return 
    1052         # Toggle signal currently disabled? 
    1053         if item.disableToggleSignal: 
    1054             return 
    1055          
    1056         addOn = item.newest 
    1057         id = addOn.id 
    1058         if item.checkState(0) == Qt.Checked:  # Mark for installation (or delete removal request) 
    1059             if id not in self.addOnsToAdd: 
    1060                 if id in self.addOnsToRemove: 
    1061                     del self.addOnsToRemove[id] 
    1062                 else: 
    1063                     self.addOnsToAdd[id] = addOn 
    1064         else:                                 # Mark for removal (or delete installation request) 
    1065             import Orange.utils.addons, orngEnviron 
    1066             installedAo = Orange.utils.addons.installed_addons[id] if id in Orange.utils.addons.installed_addons else None  
    1067             if installedAo: 
    1068                 if not installedAo.directory.startswith(orngEnviron.addOnsDirUser): 
    1069                     item.disableToggleSignal = True 
    1070                     item.setCheckState(0, Qt.Checked) 
    1071                     item.disableToggleSignal = False 
    1072                     return 
    1073             if id in self.addOnsToAdd: 
    1074                 del self.addOnsToAdd[id] 
    1075             elif id not in self.addOnsToRemove: 
    1076                 import Orange.utils.addons 
    1077                 if id in Orange.utils.addons.installed_addons: 
    1078                     self.addOnsToRemove[id] = Orange.utils.addons.installed_addons[id] 
    1079                 elif id.startswith("registered:"): 
    1080                     self.addOnsToRemove[id] = item.newest 
    1081         self.resetChecked(id)   # Refresh all checkboxes for this add-on (it might be in multiple repositories!) 
    1082         self.refreshInfoPane(item) 
    1083          
    1084     def getRepoFromItem(self, item): 
    1085         if not item: 
    1086             return None 
    1087         import Orange.utils.addons 
    1088         if hasattr(item, "repository"): 
    1089             return item.repository 
    1090         else: 
    1091             if item.newest.__class__ is not Orange.utils.addons.OrangeAddOnInRepo: 
    1092                 return None 
    1093             return  item.newest.repository 
    1094      
    1095     def getAddOnFromItem(self, item):         
    1096         if hasattr(item, "newest"): 
    1097             return item.newest 
    1098         return None 
     907    def getAddOnFromItem(self, item): 
     908        return getattr(item, "addon", None) 
    1099909 
    1100910    def getAddOnIdFromItem(self, item): 
    1101         addOn = self.getAddOnFromItem(item)         
    1102         return addOn.id if addOn else None 
     911        addon = self.getAddOnFromItem(item) 
     912        return addon.name if addon else None 
    1103913         
    1104914    def refreshInfoPane(self, item=None): 
    1105915        if not item: 
    1106             item = self.tree.currentItem() 
    1107         import Orange.utils.addons 
    1108         if hasattr(item, "newest"): 
    1109             if item.newest.__class__ is not Orange.utils.addons.OrangeRegisteredAddOn: 
    1110                 import orngEnviron 
    1111                 addOn = item.newest 
    1112                 self.lblDescription.setText(addOn.description.strip() if addOn else "") 
    1113                 self.lblVerAvailValue.setText(addOn.version_str) 
    1114      
    1115                 addOnInstalled = Orange.utils.addons.installed_addons[addOn.id] if addOn.id in Orange.utils.addons.installed_addons else None 
    1116                 addOnToInstall = self.addOnsToAdd[addOn.id] if addOn.id in self.addOnsToAdd else None 
    1117                 addOnToRemove = self.addOnsToRemove[addOn.id] if addOn.id in self.addOnsToRemove else None 
    1118                  
    1119                 self.lblVerInstalledValue.setText((addOnInstalled.version_str+("" if addOnInstalled.directory.startswith(orngEnviron.addOnsDirUser) else " (installed system-wide)")) if addOnInstalled else "-") 
    1120                 self.upgradeButton.setVisible(addOnInstalled!=None and addOnInstalled.version < addOn.version and addOnToInstall!=addOn and addOnInstalled.directory.startswith(orngEnviron.addOnsDirUser)) 
    1121                 self.donotUpgradeButton.setVisible(addOn.id in self.addOnsToRemove and addOnToInstall==addOn) 
    1122                 self.webButton.setVisible(addOn.homepage != None) 
    1123                 self.listWidgetsButton.setVisible(len(addOn.widgets) > 0 and addOn.__class__ is Orange.utils.addons.OrangeAddOnInRepo and addOn.repository.has_web_script) 
    1124                  
    1125                 if addOnToInstall: 
    1126                     if addOnToRemove: self.lblStatus.setText("marked for upgrade") 
    1127                     else: self.lblStatus.setText("marked for installation") 
    1128                 elif addOnToRemove: self.lblStatus.setText("marked for removal") 
    1129                 else: self.lblStatus.setText("") 
    1130      
    1131                 self.infoPane.setVisible(True) 
    1132                 self.regiInfoPane.setVisible(False) 
     916            item = self.lst.currentItem() 
     917        addon = None 
     918        if item: 
     919            import Orange.utils.addons 
     920            import orngEnviron 
     921            addon = self.getAddOnFromItem(item) 
     922        if addon: 
     923            self.lblDescription.setText(addon.summary.strip() +"\n"+ addon.description.strip()) 
     924            self.lblVerAvailValue.setText(addon.available_version) 
     925 
     926            self.lblVerInstalledValue.setText(addon.installed_version if addon.installed_version else "-") #TODO Tell whether it's a system-wide installation 
     927            self.upgradeButton.setVisible(bool(addon.installed_version and addon.installed_version!=addon.available_version) and addon.name not in self.to_upgrade) #TODO Disable if it's a system-wide installation 
     928            self.donotUpgradeButton.setVisible(addon.name in self.to_upgrade) 
     929            self.webButton.setVisible(bool(addon.homepage)) 
     930            self.docButton.setVisible(bool(addon.docs_url)) 
     931            self.listWidgetsButton.setVisible(False) #TODO A list of widgets is not available 
     932 
     933            if not addon.installed_version and item.checkState()==Qt.Checked: 
     934                self.lblStatus.setText("marked for installation") 
     935            elif addon.installed_version and item.checkState()!=Qt.Checked: 
     936                self.lblStatus.setText("marked for removal") 
     937            elif addon.name in self.to_upgrade: 
     938                self.lblStatus.setText("marked for upgrade") 
    1133939            else: 
    1134                 self.lblRegisteredAddOnInfo.setText("This add-on is registered "+("system-wide." if item.newest.systemwide else "by user.")) 
    1135                 self.infoPane.setVisible(False) 
    1136                 self.regiInfoPane.setVisible(True) 
     940                self.lblStatus.setText("") 
     941 
     942            self.infoPane.setVisible(True) 
     943            self.regiInfoPane.setVisible(False) 
    1137944        else: 
    1138945            self.infoPane.setVisible(False) 
    1139946            self.regiInfoPane.setVisible(False) 
    1140          
     947        self.enableDisableButtons() 
     948 
    1141949    def enableDisableButtons(self): 
    1142         repo = self.getRepoFromItem(self.tree.currentItem()) 
    1143950        import Orange.utils.addons 
    1144         self.delRepoButton.setEnabled(repo.__class__ is not Orange.utils.addons.OrangeDefaultAddOnRepository if repo!=None else False) 
    1145         self.editRepoButton.setEnabled(repo.__class__ is not Orange.utils.addons.OrangeDefaultAddOnRepository if repo!=None else False) 
    1146         self.upgradeAllButton.setEnabled(self.upgradeCandidates() != []) 
     951        aos = Orange.utils.addons.addons.values() 
     952        self.upgradeAllButton.setEnabled(any(ao.installed_version and ao.available_version and 
     953                                             ao.installed_version != ao.available_version and 
     954                                             ao.name not in self.to_upgrade for ao in aos)) 
    1147955         
    1148956    def currentItemChanged(self, new, previous): 
    1149         # Enable/disable buttons 
    1150         self.enableDisableButtons() 
    1151              
    1152         # Refresh info pane 
     957        # Refresh info pane & button states 
    1153958        self.refreshInfoPane(new) 
    1154      
    1155     def resetChecked(self, id): 
    1156         import Orange.utils.addons 
    1157         value = id in Orange.utils.addons.installed_addons or id.startswith("registered:") 
    1158         value = value and id not in self.addOnsToRemove 
    1159         value = value or id in self.addOnsToAdd 
    1160         for treeItem in self.tree.addOnItems: 
    1161             if treeItem.newest.id == id: 
    1162                 treeItem.disableToggleSignal = True 
    1163                 treeItem.setCheckState(0,Qt.Checked if value else Qt.Unchecked); 
    1164                 treeItem.disableToggleSignal = False 
    1165  
    1166     def addAddOnsToTree(self, repoItem, addOnDict, insertToBeginning=False): 
    1167         # Transform dictionary {id->[versions]} list of tuples (newest,[otherVersions]) 
    1168         if type(addOnDict) is list: 
    1169             addOnList = [(ao, []) for ao in addOnDict] 
    1170         else: 
    1171             addOnList = [] 
    1172             for id in addOnDict: 
    1173                 versions = list(addOnDict[id])  # We make a copy, so that we can change it! 
    1174                 newest = versions[0] 
    1175                 for v in versions: 
    1176                     if v.version > newest.version: 
    1177                         newest = v 
    1178                 versions.remove(newest) 
    1179                 addOnList.append( (newest, versions) ) 
     959 
     960    def addAddOnsToTree(self, addon_dict, selected=None, to_install=[], to_remove=[]): 
    1180961        # Sort alphabetically 
    1181         addOnList.sort(key=lambda (newest, versions): newest.name) 
    1182         # Single-addon packages last 
    1183         if self.sortSingleLast: 
    1184             addOnList = [(n, v) for (n, v) in addOnList if not n.has_single_widget] \ 
    1185                       + [(n, v) for (n, v) in addOnList if     n.has_single_widget] 
    1186         # Installed first 
    1187         if self.sortInstalledFirst and len(addOnList)>0 and "id" in addOnList[0][0].__dict__: 
    1188             import Orange.utils.addons 
    1189             addOnList = [(n, v) for (n, v) in addOnList if     n.id in Orange.utils.addons.installed_addons] \ 
    1190                       + [(n, v) for (n, v) in addOnList if not n.id in Orange.utils.addons.installed_addons] 
    1191          
    1192         for (i, (newest, versions)) in enumerate(addOnList): 
    1193             addOnItem = QTreeWidgetItem(repoItem if not insertToBeginning else None) 
    1194             if insertToBeginning: 
    1195                 if repoItem.__class__ is QTreeWidget: 
    1196                     repoItem.insertTopLevelItem(i, addOnItem) 
    1197                 else: 
    1198                     repoItem.insertChild(i, addOnItem) 
    1199             addOnItem.disableToggleSignal = True 
    1200             addOnItem.setText(0, newest.name) 
    1201             if newest.has_single_widget(): 
    1202                 italFont = QFont(addOnItem.font(0)) 
    1203                 italFont.setItalic(True) 
    1204                 addOnItem.setFont(0, italFont) 
    1205             addOnItem.setCheckState(0,Qt.Unchecked); 
    1206             addOnItem.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemIsEnabled) 
    1207             addOnItem.newest = newest 
    1208             addOnItem.otherVersions = versions 
    1209  
    1210             self.tree.addOnItems.append(addOnItem) 
    1211             repoItem.addOnItemsDict[newest.id] = addOnItem 
    1212             self.resetChecked(newest.id) 
    1213             addOnItem.disableToggleSignal = False 
    1214              
    1215  
    1216     def addRepositoryToTree(self, repo): 
    1217         repoItem = QTreeWidgetItem(self.tree) 
    1218         repoItem.repository = repo 
    1219         repoItem.addOnItemsDict = {} 
    1220         repoItem.setText(0, repo.name) 
    1221         boldFont = QFont(repoItem.font(0)) 
    1222         boldFont.setWeight(QFont.Bold) 
    1223         repoItem.setFont(0, boldFont) 
    1224         self.tree.repoItems[repo] = repoItem 
    1225          
    1226         addOnsToAdd = {} 
    1227         visibleAddOns = repo.search_index(self.searchStr) 
    1228         for (id, versions) in repo.addons.items(): 
    1229             if id in visibleAddOns: 
    1230                 addOnsToAdd[id] = versions 
    1231         self.addAddOnsToTree(repoItem, addOnsToAdd) 
    1232          
    1233         return repoItem 
    1234              
     962        addons = sorted(list(addon_dict.items()), 
     963                        key = lambda (name, ao): name) 
     964 
     965        for (i, (name, ao)) in enumerate(addons): 
     966            item = QListWidgetItem() 
     967            self.lst.addItem(item) 
     968            item.setText(name) 
     969            item.setCheckState(Qt.Checked if ao.installed_version and not name in to_remove or name in to_install else Qt.Unchecked) 
     970            item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsSelectable | Qt.ItemIsEnabled) 
     971            item.addon = ao 
     972            if name == selected: 
     973                self.lst.setCurrentItem(item) 
     974 
     975            item.disableToggleSignal = False 
     976 
     977    def lst_items(self): 
     978        for i in xrange(self.lst.count()): 
     979            yield self.lst.item(i) 
     980 
     981    def to_install(self): 
     982        return set([item.addon.name for item in self.lst_items() 
     983                    if item.checkState()==Qt.Checked and not item.addon.installed_version]) 
     984 
     985    def to_remove(self): 
     986        return set([item.addon.name for item in self.lst_items() 
     987                    if item.checkState()!=Qt.Checked and item.addon.installed_version]) 
    1235988 
    1236989    def refreshView(self, selectedRegisteredAddOnId=None): 
    1237         # Save repository items expanded state 
    1238         expandedRepos = set([]) 
    1239         for (repo, item) in self.tree.repoItems.items(): 
    1240             if item.isExpanded(): 
    1241                 expandedRepos.add(repo) 
     990        import Orange 
    1242991        # Save current item selection 
    1243         selectedRepository = self.getRepoFromItem(self.tree.currentItem()) 
    1244         selectedAddOnId = self.getAddOnIdFromItem(self.tree.currentItem()) 
     992        selected_addon = self.getAddOnIdFromItem(self.lst.currentItem()) 
     993        to_install = self.to_install() 
     994        to_remove = self.to_remove() 
    1245995        #TODO: Save the next repository selection too, in case the current one was deleted 
    1246996 
    1247997        # Clear the tree 
    1248         self.tree.repoItems = {} 
    1249         self.tree.addOnItems = [] 
    1250         self.tree.addOnItemsDict = {} 
    1251         self.tree.clear() 
    1252          
    1253         # Set button visibility 
    1254         self.editRepoButton.setVisible(self.groupByRepo) 
    1255         self.delRepoButton.setVisible(self.groupByRepo) 
     998        self.lst.clear() 
    1256999         
    12571000        # Add repositories and add-ons 
    1258         shownAddOns = set([]) 
    1259         if self.groupByRepo: 
    1260             for repo in self.repositories: 
    1261                 item = self.addRepositoryToTree(repo) 
    1262                 shownAddOns = shownAddOns.union(set(repo.addons).intersection(repo.search_index(self.searchStr))) 
    1263         else: 
    1264             addOns = {} 
    1265             for repo in self.repositories: 
    1266                 for addOnId in repo.addons: 
    1267                     if addOnId in repo.search_index(self.searchStr): 
    1268                         if addOnId in addOns: 
    1269                             addOns[addOnId].extend(repo.addons[addOnId]) 
    1270                         else: 
    1271                             addOns[addOnId] = list(repo.addons[addOnId]) 
    1272             self.addAddOnsToTree(self.tree, addOns) 
    1273             shownAddOns = set(addOns) 
    1274          
    1275         # Add add-ons that are not present in any repository 
    1276         if self.searchStr.strip() == "":   # but we do not need to search among installed add-ons 
    1277             import Orange.utils.addons 
    1278             onlyInstalledAddOns = {} 
    1279             for addOn in Orange.utils.addons.installed_addons.values(): 
    1280                 if addOn.id not in shownAddOns: 
    1281                     onlyInstalledAddOns[addOn.id] = [addOn] 
    1282             self.addAddOnsToTree(self.tree, onlyInstalledAddOns, insertToBeginning=True) 
    1283              
    1284         # Registered Add-ons 
    1285         if Orange.utils.addons.registered_addons != [] or any([id.startswith("registered:") for id in self.addOnsToAdd]): 
    1286             regiItem = QTreeWidgetItem(self.tree) 
    1287             regiItem.repository = None 
    1288             regiItem.addOnItemsDict = {} 
    1289             regiItem.setText(0, "Registered Add-ons") 
    1290             boldFont = QFont(regiItem.font(0)) 
    1291             boldFont.setWeight(QFont.Bold) 
    1292             regiItem.setFont(0, boldFont) 
    1293             self.tree.repoItems["Registered Add-ons"] = regiItem 
    1294              
    1295             addOnsToAdd = [] 
    1296             import re 
    1297             words = [word for word in re.split(Orange.utils.addons.index_re, self.searchStr.lower()) if word!=""] 
    1298             visibleAddOns = [ao for ao in Orange.utils.addons.registered_addons+[ao for ao in self.addOnsToAdd.values() if ao.id.startswith("registered:")] if all([word in ao.name for word in words])] 
    1299             self.addAddOnsToTree(regiItem, visibleAddOns) 
    1300             if selectedRegisteredAddOnId: 
    1301                 regiItem.setExpanded(True) 
    1302                 self.tree.setCurrentItem(regiItem.addOnItemsDict[selectedRegisteredAddOnId]) 
    1303              
    1304         # Restore repository items expanded state 
    1305         if len(expandedRepos)==0: 
    1306             self.tree.expandItem(self.tree.topLevelItem(0)) 
    1307         else: 
    1308             for (repo, item) in self.tree.repoItems.items(): 
    1309                 if repo in expandedRepos: 
    1310                     item.setExpanded(True) 
    1311                      
    1312         # Restore item selection 
    1313         if not selectedRegisteredAddOnId: 
    1314             select = self.tree.topLevelItem(0) 
    1315             search = None 
    1316             if selectedRepository in self.tree.repoItems: 
    1317                 select = self.tree.repoItems[selectedRepository] 
    1318                 search = select 
    1319             elif not selectedRepository: 
    1320                 search = self.tree 
    1321             if selectedAddOnId and search: 
    1322                 if selectedAddOnId in search.addOnItemsDict: 
    1323                     select = search.addOnItemsDict[selectedAddOnId] 
    1324             self.tree.setCurrentItem(select) 
    1325              
     1001        addons = {} 
     1002        for name in Orange.utils.addons.search_index(self.searchStr): 
     1003            addons[name] = Orange.utils.addons.addons[name] 
     1004        self.addAddOnsToTree(addons, selected = selected_addon, to_install=to_install, to_remove=to_remove) 
     1005        self.refreshInfoPane() 
     1006 
     1007        #TODO Should we somehow show the legacy registered addons? 
     1008 
    13261009    def searchCallback(self): 
    13271010        self.refreshView() 
  • Orange/OrangeCanvas/orngRegistry.py

    r10879 r11018  
    2828            return os.path.join(orngEnviron.orangeDocDir, "widgets", subDir) 
    2929        else:  # An add-on widget 
    30             addOnDocDir = self.addOn.directory_documentation() 
    31             return os.path.join(addOnDocDir, "widgets") 
     30            return None  # new style add-ons only have on-line documentation 
     31            #addOnDocDir = self.addOn.directory_documentation() 
     32            #return os.path.join(addOnDocDir, "widgets") 
    3233 
    3334 
     
    3738            self.update(widgets) 
    3839        self.name = name 
    39     
     40 
     41def load_new_addons(directories = []): 
     42    # New-type add-ons 
     43    for entry_point in pkg_resources.iter_entry_points(WIDGETS_ENTRY_POINT): 
     44        try: 
     45            module = entry_point.load() 
     46            if hasattr(module, '__path__'): 
     47                # It is a package 
     48                directories.append((entry_point.name, module.__path__[0], entry_point.name, False, module)) 
     49            else: 
     50                # It is a module 
     51                # TODO: Implement loading of widget modules 
     52                # (This should be default way to load widgets, not parsing them as files, or traversing directories, just modules and packages (which load modules)) 
     53                pass 
     54        except ImportError, err: 
     55            print "While loading, importing widgets '%s' failed: %s" % (entry_point.name, err) 
     56        except pkg_resources.DistributionNotFound, err: 
     57            print "Loading add-on '%s' failed because of a missing dependency: '%s'" % (entry_point.name, err) 
     58        except Exception, err: 
     59            print "An exception occurred during the loading of '%s':\n%r" %(entry_point.name, err) 
     60    return directories 
     61 
    4062def readCategories(silent=False): 
    4163    try: 
     
    7799             
    78100    # 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         except pkg_resources.DistributionNotFound, err: 
    105             print "Loading add-on '%s' failed because of a missing dependency: '%s'" % (entry_point.name, err) 
    106         except Exception, err: 
    107             print "An exception occurred during the loading of '%s':\n%r" %(entry_point.name, err) 
     101    #TODO Load registered add-ons! 
     102 
     103    load_new_addons(directories) 
    108104 
    109105    categories = {}      
     
    272268                    formatedOutList += " &nbsp; &nbsp; - " + signal.name + " (" + signal.type + ")<br>" 
    273269 
    274             addOnName = "" if not widgetInfo.addOn else " (from add-on %s)" % widgetInfo.addOn.name 
     270            addOnName = "" if not widgetInfo.addOn else " (from add-on %s)" % widgetInfo.addOn 
    275271     
    276272            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])  
  • Orange/utils/addons.py

    r10581 r11018  
     1from __future__ import absolute_import 
    12""" 
    23============================== 
     
    910soon as it is imported, the following initialization takes place: the list of 
    1011installed add-ons is loaded, their directories are added to python path 
    11 (:obj:`sys.path`) the callback list is initialized the stored repository list is 
     12(:obj:`sys.path`) the callback list is initialized, the stored repository list is 
    1213loaded. The most important consequence of importing the module is thus the 
    13 ability to import add-ons' modules, because they are now in the python path. 
    14  
    15 .. attribute:: available_repositories 
    16  
    17    List of add-on repository descriptors (instances of 
    18    :class:`OrangeAddOnRepository`). 
    19  
    20 .. attribute:: addon_directories 
    21  
    22    List of directories that have been added to the path to make use of add-ons 
    23    possible; see :obj:`add_addon_directories_to_path`. 
    24  
    25 .. attribute:: registered_addons 
    26  
    27    A list of registered add-on descriptors (instances of 
    28    :class:`OrangeRegisteredAddOn`). 
    29  
    30 .. attribute:: available_addons 
    31  
    32    A dictionary mapping URLs of repositories to instances of 
    33    :class:`OrangeAddOnRepository`. 
    34  
    35 .. attribute:: installed_addons 
    36  
    37    A dictionary mapping GUIDs to instances of :class:`OrangeAddOnInstalled`. 
    38  
    39 .. autofunction:: load_installed_addons_from_dir 
    40  
    41 .. autofunction:: repository_list_filename 
    42  
    43 .. autofunction:: load_repositories 
    44  
    45 .. autofunction:: save_repositories 
    46  
    47 .. autofunction:: update_default_repositories 
    48  
    49 .. autofunction:: add_addon_directories_to_path 
    50  
    51 .. autofunction:: install_addon 
    52  
    53 .. autofunction:: install_addon_from_repo 
    54  
    55 .. autofunction:: load_addons 
    56  
    57 .. autofunction:: refresh_addons 
    58  
    59 .. autofunction:: register_addon 
    60  
    61 .. autofunction:: unregister_addon 
    62  
    63 Add-on descriptors and packaging routines 
    64 ========================================= 
    65  
    66 .. autofunction:: suggest_version 
    67  
    68 .. autoclass:: OrangeRegisteredAddOn 
    69    :members: 
    70    :show-inheritance: 
    71  
    72 .. autoclass:: OrangeAddOn 
    73    :members: 
    74    :show-inheritance: 
    75  
    76 .. autoclass:: OrangeAddOnInRepo 
    77    :members: 
    78    :show-inheritance: 
    79  
    80 .. autoclass:: OrangeAddOnInstalled 
    81    :members: 
    82    :show-inheritance: 
    83  
    84 Add-on repository descriptors 
    85 ============================= 
    86  
    87 .. autoclass:: OrangeAddOnRepository 
    88    :members: 
    89    :show-inheritance: 
    90     
    91 .. autoclass:: OrangeDefaultAddOnRepository 
    92    :members: 
    93    :show-inheritance: 
    94  
    95 Exception classes 
    96 ================= 
    97  
    98 .. autoclass:: RepositoryException 
    99    :members: 
    100    :show-inheritance: 
    101  
    102 .. autoclass:: InstallationException 
    103    :members: 
    104    :show-inheritance: 
    105  
    106 .. autoclass:: PackingException 
    107    :members: 
    108    :show-inheritance: 
     14injection of add-ons into the namespace. 
    10915 
    11016""" 
    11117 
    112  
    113 import xml.dom.minidom 
     18#TODO Document this module. 
     19 
     20import socket 
     21import shelve 
     22import xmlrpclib 
     23import warnings 
    11424import re 
     25import pkg_resources 
     26import tempfile 
     27import tarfile 
     28import shutil 
    11529import os 
    116 import shutil 
    11730import sys 
    118 import glob 
    119 import time 
    120 import socket 
    121 import urllib  # urllib because we need 'urlretrieve' 
    122 import urllib2 # urllib2 because it reports HTTP Errors for 'urlopen' 
    123 import bisect 
    12431import platform 
     32from collections import namedtuple, defaultdict 
    12533 
    12634import Orange.utils.environ 
    127 import widgetparser 
    128 from fileutil import * 
    129 from fileutil import _zip_open 
    130 from zipfile import ZipFile 
    131  
    132 import warnings 
     35 
     36ADDONS_ENTRY_POINT="orange.addons" 
    13337 
    13438socket.setdefaulttimeout(120)  # In seconds. 
    13539 
    136 class PackingException(Exception): 
    137     """ 
    138     An exception that occurs during add-on packaging. Behaves exactly as 
    139     :class:`Exception`. 
    140      
    141     """ 
    142     pass 
    143  
    144 def suggest_version(current_version): 
    145     """ 
    146     Automatically construct a version string of form "year.month.day[.number]".  
    147     If the passed "current version" is already in this format and contains 
    148     identical date, the last number is incremented if it exists; otherwise ".1" 
    149     is appended. 
    150      
    151     :param current_version: version on which to base the new version; is used 
    152         only in case it is in the same format. 
    153     :type current_version: str 
    154      
    155     """ 
    156      
    157     version = time.strftime("%Y.%m.%d") 
    158     try: 
    159         xmlver_int = map(int, current_version.split(".")) 
     40OrangeAddOn = namedtuple('OrangeAddOn', ['name', 'available_version', 'installed_version', 'summary', 'description', 
     41                                         'author', 'docs_url', 'keywords', 'homepage', 'package_url', 
     42                                         'release_url', 'release_size', 'python_version']) 
     43#It'd be great if we could somehow read a list and descriptions of widgets, show them in the dialog and enable 
     44#search of add-ons based on keywords in widget names and descriptions. 
     45 
     46INDEX_RE = "[^a-z0-9-']"  # RE for splitting entries in the search index 
     47 
     48AOLIST_FILE = os.path.join(Orange.utils.environ.orange_settings_dir, "addons.shelve") 
     49try: 
     50    addons = shelve.open(AOLIST_FILE) 
     51    list(addons.items())  # Try to read the whole list. 
     52except: 
     53    addons = shelve.open(AOLIST_FILE, 'n') 
     54 
     55addon_refresh_callback = [] 
     56 
     57global index 
     58index = defaultdict(list) 
     59def rebuild_index(): 
     60    global index 
     61 
     62    index = defaultdict(list) 
     63    for name, ao in addons.items(): 
     64        for s in [name, ao.summary, ao.description, ao.author] + (ao.keywords if ao.keywords else []): 
     65            if not s: 
     66                continue 
     67            words = [word for word in re.split(INDEX_RE, s.lower()) 
     68                     if len(word)>1] 
     69            for word in words: 
     70                for i in range(len(word)): 
     71                    index[word[:i+1]].append(name) 
     72 
     73def search_index(query): 
     74    global index 
     75    result = set() 
     76    words = [word for word in re.split(INDEX_RE, query.lower()) if len(word)>1] 
     77    if not words: 
     78        return addons.keys() 
     79    for word in words: 
     80        result.update(index[word]) 
     81    return result 
     82 
     83def refresh_available_addons(force=False): 
     84    pypi = xmlrpclib.ServerProxy('http://pypi.python.org/pypi') 
     85 
     86    pkg_dict = {} 
     87    for data in pypi.search({'keywords': 'orange'}): 
     88        name = data['name'] 
     89        order = data['_pypi_ordering'] 
     90        if name not in pkg_dict or pkg_dict[name][0] < order: 
     91            pkg_dict[name] = (order, data['version']) 
     92 
     93    try: 
     94        import slumber 
     95        readthedocs = slumber.API(base_url='http://readthedocs.org/api/v1/') 
    16096    except: 
    161         xmlver_int = [] 
    162     ver_int = map(int, version.split(".")) 
    163     if xmlver_int[:3] == ver_int[:3]: 
    164         version += ".%d" % ((xmlver_int[3] if len(xmlver_int)>3 else 0) +1) 
    165     return version 
    166  
    167 class OrangeRegisteredAddOn(): 
    168     """ 
    169     An add-on that is not linked to an on-line repository, but resides in an 
    170     independent directory and has been registered in Orange to be loaded when 
    171     Canvas is run. Helper methods are also implemented to enable packaging of 
    172     a registered add-on into an .oao package, including methods to generate 
    173     a skeleton of documentation files. 
    174      
    175     .. attribute:: id 
    176      
    177        ID of the add-on. IDs of registered add-ons are in form 
    178        "registered:<dir>", where <dir> is the directory of add-on's files. 
    179      
    180     .. attribute:: name 
    181         
    182        name of the add-on. 
    183         
    184     .. attribute:: directory 
    185      
    186        the directory where the add-on's files reside. 
    187      
    188     .. attribute:: systemwide 
    189      
    190        a flag indicating whether the add-on is registered system-wide, i.e. 
    191        for all OS users. 
    192      
    193     """ 
    194      
    195     def __init__(self, name, directory, systemwide=False): 
    196         """ 
    197         Constructor only sets the attributes. 
    198          
    199         :param name: name of the add-on. 
    200         :type name: str 
    201          
    202         :param directory: full path to the add-on's files. 
    203         :type directory: str 
    204          
    205         :param systemwide: determines whether the add-on is installed 
    206             systemwide, ie. for all users. 
    207         :type systemwide: boolean 
    208         """ 
    209         self.name = name 
    210         self.directory = directory 
    211         self.systemwide = systemwide 
    212          
    213         # Imitate real add-ons behaviour 
    214         self.id = "registered:"+directory 
    215  
    216     # Imitate real add-ons behaviour 
    217     def has_single_widget(self): 
    218         """ 
    219         Always return False: this feature is not implemented for registered 
    220         add-ons. 
    221         """ 
    222         return False 
    223  
    224     def directory_documentation(self): 
    225         """ 
    226         Return the documentation directory -- the "doc" directory under the 
    227         add-on's directory. 
    228         """ 
    229         return os.path.join(self.directory, "doc") 
    230  
    231     def uninstall(self, refresh=True): 
    232         """ 
    233         Uninstall, or rather unregister, the registered add-on. The files in 
    234         add-on's directory are not deleted or in any other way changed. 
    235          
    236         :param refresh: determines whether add-on list change callback 
    237             functions are to be called after the unregistration process. This 
    238             should always be True, except when multiple operations are executed 
    239             in a batch. 
    240         :type refresh: boolean 
    241         """ 
     97        readthedocs = None 
     98 
     99    docs = {} 
     100    for name, (_, version) in pkg_dict.items(): 
     101        if force or name not in addons or addons[name].available_version != version: 
     102            try: 
     103                data = pypi.release_data(name, version) 
     104                rel = pypi.release_urls(name, version)[0] 
     105 
     106                if readthedocs: 
     107                    try: 
     108                        docs = readthedocs.project.get(slug=name.lower())['objects'][0] 
     109                    except: 
     110                        docs = {} 
     111                addons[name] = OrangeAddOn(name = name, 
     112                                           available_version = data['version'], 
     113                                           installed_version = addons[name].installed_version if name in addons else None, 
     114                                           summary = data['summary'], 
     115                                           description = data.get('description', ''), 
     116                                           author = str((data.get('author', '') or '') + ' ' + (data.get('author_email', '') or '')).strip(), 
     117                                           docs_url = data.get('docs_url', docs.get('subdomain', '')), 
     118                                           keywords = data.get('keywords', "").split(","), 
     119                                           homepage = data.get('home_page', ''), 
     120                                           package_url = data.get('package_url', ''), 
     121                                           release_url = rel.get('url', None), 
     122                                           release_size = rel.get('size', -1), 
     123                                           python_version = rel.get('python_version', None)) 
     124            except Exception, e: 
     125                import traceback 
     126                traceback.print_exc() 
     127                warnings.warn('Could not load data for the following add-on: %s'%name) 
     128 
     129    rebuild_index() 
     130 
     131def load_installed_addons(): 
     132    #TODO Unload existing or what? 
     133 
     134    found = set() 
     135    for entry_point in pkg_resources.iter_entry_points(ADDONS_ENTRY_POINT): 
     136        name, version = entry_point.dist.project_name, entry_point.dist.version 
     137        #TODO We could import setup.py from entry_point.location and load descriptions and such ... 
     138        if name in addons: 
     139            addons[name] = addons[name]._replace(installed_version = version) 
     140        else: 
     141            addons[name] = OrangeAddOn(name = name, 
     142                available_version = None, 
     143                installed_version = version, 
     144                summary = None, 
     145                description = None, 
     146                author = None, 
     147                docs_url = None, 
     148                keywords = None, 
     149                homepage = None, 
     150                package_url = None, 
     151                release_url = None, 
     152                release_size = None, 
     153                python_version = None)  # We should find all those values out from the setup.py or somewhere ... 
     154        found.add(name) 
     155    for name in set(addons).difference(found): 
     156        addons[name] = addons[name]._replace(installed_version = None) 
     157    rebuild_index() 
     158 
     159def run_setup(setup_script, args): 
     160    old_dir = os.getcwd() 
     161    save_argv = sys.argv[:] 
     162    save_path = sys.path[:] 
     163    setup_dir = os.path.abspath(os.path.dirname(setup_script)) 
     164    temp_dir = os.path.join(setup_dir,'temp') 
     165    if not os.path.isdir(temp_dir): os.makedirs(temp_dir) 
     166    save_tmp = tempfile.tempdir 
     167    save_modules = sys.modules.copy() 
     168    try: 
     169        tempfile.tempdir = temp_dir 
     170        os.chdir(setup_dir) 
    242171        try: 
    243             unregister_addon(self.name, self.directory, user_only=True)             
    244             if refresh: 
    245                 refresh_addons() 
    246             return True 
     172            sys.argv[:] = [setup_script]+list(args) 
     173            sys.path.insert(0, setup_dir) 
     174            execfile( 
     175                    "setup.py", 
     176                    {'__file__':setup_script, '__name__':'__main__'} 
     177                ) 
     178        except SystemExit, v: 
     179            if v.args and v.args[0]: 
     180                raise 
     181                # Normal exit, just return 
     182    finally: 
     183        sys.modules.update(save_modules) 
     184        for key in list(sys.modules): 
     185            if key not in save_modules: del sys.modules[key] 
     186        os.chdir(old_dir) 
     187        sys.path[:] = save_path 
     188        sys.argv[:] = save_argv 
     189        tempfile.tempdir = save_tmp 
     190 
     191 
     192def install(name): 
     193    import site 
     194    try: 
     195        import urllib 
     196        egg = urllib.urlretrieve(addons[name].release_url)[0] 
     197    except Exception, e: 
     198        raise Exception("Unable to download add-on from repository: %s" % e) 
     199 
     200    try: 
     201        try: 
     202            tmpdir = tempfile.mkdtemp() 
     203            egg_contents = tarfile.open(egg) 
     204            egg_contents.extractall(tmpdir) 
     205            setup_py = os.path.join(tmpdir, name+'-'+addons[name].available_version, 'setup.py') 
    247206        except Exception, e: 
    248             raise InstallationException("Unable to unregister add-on: %s" % 
    249                                         (self.name, e)) 
    250  
    251     def prepare(self, id=None, name=42, version="auto", description=None, 
    252                 tags=None, author_organizations=None, author_creators=None, 
    253                 author_contributors=None, preferred_directory=None, 
    254                 homepage=None): 
    255         """ 
    256         Prepare the add-on for packaging into an .oao ZIP file and add the 
    257         necessary files to the add-on directory (possibly overwriting some!). 
    258  
    259         :param id: ID of the add-on. Must be a valid GUID; None means it is 
    260             retained from existing addon.xml if it exists, otherwise a new GUID 
    261             is generated. 
    262         :type id: str 
    263          
    264         :param name: name of the add-on; None retains existing value if it 
    265             exists and raises exception otherwise; the default value of 42 
    266             uses :obj:`self.name`. 
    267         :type name: str 
    268              
    269         :param version: version of the add-on. None retains existing value if 
    270             it exists and does the same as "auto" otherwise; "auto" generates a 
    271             new version number from the current date in format 'yyyy.mm.dd' 
    272             (see :obj:`Orange.utils.addons.suggest_version`); if that is equal 
    273             to the current version, another integer component is appended. 
    274         :type version: str 
    275          
    276         :param description: add-on's description. None retains existing value 
    277             if it exists and raises an exception otherwise. 
    278         :type description: str 
    279          
    280         :param tags: tags; None retains existing value if it exists, else 
    281             defaults to []. 
    282         :type tags: list of str 
    283          
    284         :param author_organizations: list of authoring organizations. None 
    285             retains existing value if it exists, else defaults to []. 
    286         :type author_organizations: list of str 
    287          
    288         :param author_creators: list of names of authors. None 
    289             retains existing value if it exists, else defaults to []. 
    290         :type author_creators: list of str 
    291  
    292         :param author_contributors: list of additional organizations or people 
    293             that have contributed to the add-on development. None 
    294             retains existing value if it exists, else defaults to []. 
    295         :type author_contributors: list of str 
    296  
    297         :param preferred_directory: default directory name for installation. 
    298             None retains existing value, "" removes the tag from the XML. 
    299         :type preferred_directory: str 
    300              
    301         :param homepage: the URL of add-on's website. None retains existing 
    302             value, "" removes the tag from the XML. 
    303         :type homepage: str 
    304         """ 
    305         ########################## 
    306         # addon.xml maintenance. # 
    307         ########################## 
    308         addon_xml_path = os.path.join(self.directory, "addon.xml") 
     207            raise Exception("Unable to unpack add-on: %s" % e) 
     208 
     209        if not os.path.isfile(setup_py): 
     210            raise Exception("Unable to install add-on - it is not properly packed.") 
     211 
    309212        try: 
    310             xmldoc = xml.dom.minidom.parse(addon_xml_path) 
     213            switches = [] 
     214            if site.USER_SITE in sys.path:   # we're not in a virtualenv 
     215                switches.append('--user') 
     216            run_setup(setup_py, ['install'] + switches) 
    311217        except Exception, e: 
    312             warnings.warn("Could not load addon.xml because \"%s\"; a new one "+ 
    313                           "will be created." % e, Warning, 0) 
    314             impl = xml.dom.minidom.getDOMImplementation() 
    315             xmldoc = impl.createDocument(None, "OrangeAddOn", None) 
    316         xmldoc_root = xmldoc.documentElement 
    317         # GUID 
    318         if not id and not xml_text_of("id", parent=xmldoc_root): 
    319             # GUID needs to be generated 
    320             import uuid 
    321             id = str(uuid.uuid1()) 
    322         if id: 
    323             xml_set(xmldoc_root, "id", id) 
    324         # name 
    325         if name==42: 
    326             name = self.name 
    327         if name and name.strip(): 
    328             xml_set(xmldoc_root, "name", name.strip()) 
    329         elif not xml_text_of("name", parent=xmldoc_root): 
    330             raise PackingException("'name' is a mandatory value!") 
    331         name = xml_text_of("name", parent=xmldoc_root) 
    332         # version 
    333         xml_version = xml_text_of("version", parent=xmldoc_root) 
    334         if not xml_version and not version: 
    335             version = "auto" 
    336         if version == "auto": 
    337             version = suggest_version(xml_version) 
    338         if version: 
    339             xml_set(xmldoc_root, "version", version) 
    340         # description 
    341         meta = get_element_nonrecursive(xmldoc_root, "meta", create=True) 
    342         if description and description.strip(): 
    343             xml_set(meta, "description", description.strip()) 
    344         elif not xml_text_of("description", parent=meta): 
    345             raise PackingException("'description' is a mandatory value!") 
    346         # tags 
    347         def update_list(root, node_name, list): 
    348             listNode = get_element_nonrecursive(root, node_name) 
    349             while listNode: 
    350                 root.removeChild(listNode) 
    351                 listNode = get_element_nonrecursive(root, node_name) 
    352             for value in list: 
    353                 root.appendChild(create_text_element(node_name, value)) 
    354         if tags!=None: 
    355             tags_node = get_element_nonrecursive(meta, "tags", create=True) 
    356             update_list(tags_node, "tag", tags) 
    357         # authors 
    358         if author_organizations!=None or author_contributors!=None or \ 
    359            author_creators!=None: 
    360             authorsNode = get_element_nonrecursive(meta, "authors", create=True) 
    361             if author_organizations!=None: update_list(authorsNode, 
    362                                                        "organization", 
    363                                                        author_organizations) 
    364             if author_creators!=None:      update_list(authorsNode, 
    365                                                        "creator", 
    366                                                        author_creators) 
    367             if author_contributors!=None:  update_list(authorsNode, 
    368                                                        "contributor", 
    369                                                        author_contributors) 
    370         #  preferred_directory 
    371         if preferred_directory != None: 
    372             xml_set(xmldoc_root, "preferred_directory", preferred_directory 
    373                     if preferred_directory else None) 
    374         #  homepage 
    375         if homepage != None: 
    376             xml_set(xmldoc_root, "homepage", homepage if homepage else None) 
    377              
    378         import codecs 
    379         xmldoc.writexml(codecs.open(addon_xml_path, 'w', "utf-8"), 
    380                         encoding="UTF-8") 
    381         sys.stderr.write("Updated addon.xml written.\n") 
    382  
    383         ########################## 
    384         # style.css creation     # 
    385         ########################## 
    386         localcss = os.path.join(self.directory_documentation(), "style.css") 
    387         orangecss = os.path.join(Orange.utils.environ.doc_install_dir, "style.css") 
    388         if not os.path.isfile(localcss): 
    389             if os.path.isfile(orangecss): 
    390                 import shutil 
    391                 shutil.copy(orangecss, localcss) 
    392                 sys.stderr.write("doc/style.css created.\n") 
    393             else: 
    394                 raise PackingException("Could not find style.css in orange"+\ 
    395                                        " documentation directory.") 
    396  
    397         ########################## 
    398         # index.html creation    # 
    399         ########################## 
    400         if not os.path.isdir(self.directory_documentation()): 
    401             os.mkdir(self.directory_documentation()) 
    402         hasIndex = False 
    403         for fname in ["main", "index", "default"]: 
    404             for ext in ["html", "htm"]: 
    405                 hasIndex = hasIndex or os.path.isfile(os.path.join(self.directory_documentation(), 
    406                                                                    fname+"."+ext)) 
    407         if not hasIndex: 
    408             indexFile = open( os.path.join(self.directory_documentation(), 
    409                                            "index.html"), 'w') 
    410             indexFile.write('<html><head><link rel="stylesheet" '+\ 
    411                             'href="style.css" type="text/css" /><title>%s'+\ 
    412                             '</title></head><body><h1>Module Documentation'+\ 
    413                             '</h1>%s</body></html>' % (name+" Orange Add-on "+ \ 
    414                                                        "Documentation", 
    415                             "This is where technical add-on module "+\ 
    416                             "documentation is. Well, at least it <i>should</i>"+\ 
    417                             " be.")) 
    418             indexFile.close() 
    419             sys.stderr.write("doc/index.html written.\n") 
    420              
    421         ########################## 
    422         # iconlist.html creation # 
    423         ########################## 
    424         wdocdir = os.path.join(self.directory_documentation(), "widgets") 
    425         if not os.path.isdir(wdocdir): os.mkdir(wdocdir) 
    426         open(os.path.join(wdocdir, "index.html"), 'w').write(self.iconlist_html()) 
    427         sys.stderr.write("Widget list (doc/widgets/index.html) written.\n") 
    428  
    429         ########################## 
    430         # copying the icons      # 
    431         ########################## 
    432         icondir = os.path.join(self.directory, "widgets", "icons") 
    433         icondocdir = os.path.join(wdocdir, "icons") 
    434         proticondir = os.path.join(self.directory, "widgets", "prototypes", 
    435                                    "icons") 
    436         proticondocdir = os.path.join(wdocdir, "prototypes", "icons") 
    437  
    438         import shutil 
    439         iconbg_file = os.path.join(Orange.utils.environ.icons_install_dir, "background_32.png") 
    440         iconun_file = os.path.join(Orange.utils.environ.icons_install_dir, "Unknown.png") 
    441         if not os.path.isdir(icondocdir): os.mkdir(icondocdir) 
    442         if os.path.isfile(iconbg_file): shutil.copy(iconbg_file, icondocdir) 
    443         if os.path.isfile(iconun_file): shutil.copy(iconun_file, icondocdir) 
    444          
    445         if os.path.isdir(icondir): 
    446             import distutils.dir_util 
    447             distutils.dir_util.copy_tree(icondir, icondocdir) 
    448         if os.path.isdir(proticondir): 
    449             import distutils.dir_util 
    450             if not os.path.isdir(os.path.join(wdocdir, "prototypes")): 
    451                 os.mkdir(os.path.join(wdocdir, "prototypes")) 
    452             if not os.path.isdir(proticondocdir): os.mkdir(proticondocdir) 
    453             distutils.dir_util.copy_tree(proticondir, proticondocdir) 
    454         sys.stderr.write("Widget icons copied to doc/widgets/.\n") 
    455  
    456  
    457     ##################################################### 
    458     # What follows are ugly HTML generators.            # 
    459     ##################################################### 
    460     def widget_doc_skeleton(self, widget, prototype=False): 
    461         """ 
    462         Return an HTML skeleton for documentation of a widget. 
    463          
    464         :param widget: widget metadata. 
    465         :type widget: :class:`widgetparser.WidgetMetaData` 
    466          
    467         :param prototype: determines, whether this is a prototype widget. This 
    468             is important to generate appropriate relative paths to the icons and 
    469             CSS. 
    470         :type prototype: boolean 
    471         """ 
    472         wfile = os.path.splitext(os.path.split(widget.filename)[1])[0][2:] 
    473         pathprefix = "../" if prototype else "" 
    474         iconcode = '\n<p><img class="screenshot" style="z-index:2; border: none; height: 32px; width: 32px; position: relative" src="%s" title="Widget: %s" width="32" height="32" /><img class="screenshot" style="margin-left:-32px; z-index:1; border: none; height: 32px; width: 32px; position: relative" src="%sicons/background_32.png" width="32" height="32" /></p>' % (widget.icon, widget.name, pathprefix) 
    475          
    476         inputscode = """<DT>(None)</DT>""" 
    477         outputscode = """<DT>(None)</DT>""" 
    478         il, ol = eval(widget.inputList), eval(widget.outputList) 
    479         if il: 
    480             inputscode = "\n".join(["<dt>%s (%s)</dt>\n<dd>Describe here, what this input does.</dd>\n" % (p[0], p[1]) for p in il]) 
    481         if ol: 
    482             outputscode = "\n".join(["<dt>%s (%s)</dt>\n<dd>Describe here, what this output does.</dd>\n" % (p[0], p[1]) for p in ol]) 
    483         html = """<html> 
    484 <head> 
    485 <title>%s</title> 
    486 <link rel=stylesheet href="%s../style.css" type="text/css" media=screen> 
    487 </head> 
    488  
    489 <body> 
    490  
    491 <h1>%s</h1> 
    492 %s 
    493 <p>This widget does this and that..</p> 
    494  
    495 <h2>Channels</h2> 
    496  
    497 <h3>Inputs</h3> 
    498  
    499 <dl class=attributes> 
    500 %s 
    501 </dl> 
    502  
    503 <h3>Outputs</h3> 
    504 <dl class=attributes> 
    505 %s 
    506 </dl> 
    507  
    508 <h2>Description</h2> 
    509  
    510 <!-- <img class="leftscreenshot" src="%s.png" align="left"> --> 
    511  
    512 <p>This is a widget which ...</p> 
    513  
    514 <p>If you press <span class="option">Reload</span>, something will happen. <span class="option">Commit</span> button does something else.</p> 
    515  
    516 <h2>Examples</h2> 
    517  
    518 <p>This widget is used in this and that way. It often gets data from 
    519 the <a href="Another.htm">Another Widget</a>.</p> 
    520  
    521 <!-- <img class="schema" src="%s-Example.png" alt="Schema with %s widget"> --> 
    522  
    523 </body> 
    524 </html>""" % (widget.name, pathprefix, widget.name, iconcode, inputscode, 
    525               outputscode, wfile, wfile, widget.name) 
    526         return html 
    527          
    528      
    529     def iconlist_html(self, create_skeleton_docs=True): 
    530         """ 
    531         Prepare and return an HTML document, containing a table of widget icons. 
    532          
    533         :param create_skeleton_docs: determines whether documentation skeleton for 
    534             widgets without documentation should be generated (ie. whether the 
    535             method :obj:`widget_doc_skeleton` should be called. 
    536         :type create_skeleton_docs: boolean 
    537         """ 
    538         html = """ 
    539 <style> 
    540 div#maininner { 
    541   padding-top: 25px; 
    542 } 
    543  
    544 div.catdiv h2 { 
    545   border-bottom: none; 
    546   padding-left: 20px; 
    547   padding-top: 5px; 
    548   font-size: 14px; 
    549   margin-bottom: 5px; 
    550   margin-top: 0px; 
    551   color: #fe6612; 
    552 } 
    553  
    554 div.catdiv { 
    555   margin-left: 10px; 
    556   margin-right: 10px; 
    557   margin-bottom: 20px; 
    558   background-color: #eeeeee; 
    559 } 
    560  
    561 div.catdiv table { 
    562   width: 98%; 
    563   margin: 10px; 
    564   padding-right: 20px; 
    565 } 
    566  
    567 div.catdiv table td { 
    568   background-color: white; 
    569 /*  height: 18px;*/ 
    570   margin: 25px; 
    571   vertical-align: center; 
    572   border-left: solid #eeeeee 10px; 
    573   border-bottom: solid #eeeeee 3px; 
    574   font-size: 13px; 
    575 } 
    576  
    577 div.catdiv table td.left { 
    578   width: 3%; 
    579   height: 28px; 
    580   padding: 0; 
    581   margin: 0; 
    582 } 
    583  
    584 div.catdiv table td.left-nodoc { 
    585   width: 3%; 
    586   color: #aaaaaa; 
    587   padding: 0; 
    588   margin: 0 
    589 } 
    590  
    591  
    592 div.catdiv table td.right { 
    593   padding-left: 5px; 
    594   border-left: none; 
    595   width: 22%; 
    596   font-size: 11px; 
    597 } 
    598  
    599 div.catdiv table td.right-nodoc { 
    600   width: 22%; 
    601   padding-left: 5px; 
    602   border-left: none; 
    603   color: #aaaaaa; 
    604   font-size: 11px; 
    605 } 
    606  
    607 div.catdiv table td.empty { 
    608   background-color: #eeeeee; 
    609 } 
    610  
    611  
    612 .rnd1 { 
    613  height: 1px; 
    614  border-left: solid 3px #ffffff; 
    615  border-right: solid 3px #ffffff; 
    616  margin: 0px; 
    617  padding: 0px; 
    618 } 
    619  
    620 .rnd2 { 
    621  height: 2px; 
    622  border-left: solid 1px #ffffff; 
    623  border-right: solid 1px #ffffff; 
    624  margin: 0px; 
    625  padding: 0px; 
    626 } 
    627  
    628 .rnd11 { 
    629  height: 1px; 
    630  border-left: solid 1px #eeeeee; 
    631  border-right: solid 1px #eeeeee; 
    632  margin: 0px; 
    633  padding: 0px; 
    634 } 
    635  
    636 .rnd1l { 
    637  height: 1px; 
    638  border-left: solid 1px white; 
    639  border-right: solid 1px #eeeeee; 
    640  margin: 0px; 
    641  padding: 0px; 
    642 } 
    643  
    644 div.catdiv table img { 
    645   border: none; 
    646   height: 28px; 
    647   width: 28px; 
    648   position: relative; 
    649 } 
    650 </style> 
    651  
    652 <script> 
    653 function setElColors(t, id, color) { 
    654   t.style.backgroundColor=document.getElementById('cid'+id).style.backgroundColor = color; 
    655 } 
    656 </script> 
    657  
    658 <p style="font-size: 16px; font-weight: bold">Catalog of widgets</p> 
    659         """ 
    660         wdir = os.path.join(self.directory, "widgets") 
    661         pdir = os.path.join(wdir, "prototypes") 
    662         widgets = {} 
    663         for (prototype, filename) in [(False, filename) for filename in 
    664                                       glob.iglob(os.path.join(wdir, "*.py"))] +\ 
    665                                      [(True, filename) for filename in 
    666                                       glob.iglob(os.path.join(pdir, "*.py"))]: 
    667             if os.path.isdir(filename): 
    668                 continue 
    669             try: 
    670                 meta =widgetparser.WidgetMetaData(file(filename).read(), 
    671                                                    "Prototypes" if prototype else "Uncategorized", 
    672                                                    enforceDefaultCategory=prototype, 
    673                                                    filename=filename) 
    674             except: 
    675                 continue # Probably not an Orange Widget module; skip this file. 
    676             if meta.category in widgets: 
    677                 widgets[meta.category].append((prototype, meta)) 
    678             else: 
    679                 widgets[meta.category] = [(prototype, meta)] 
    680         category_list = [cat for cat in widgets.keys() 
    681                          if cat not in ["Prototypes", "Uncategorized"]] 
    682         category_list.sort() 
    683         for cat in ["Uncategorized"] + category_list + ["Prototypes"]: 
    684             if cat not in widgets: 
    685                 continue 
    686             html += """    <div class="catdiv"> 
    687     <div class="rnd1"></div> 
    688     <div class="rnd2"></div> 
    689  
    690     <h2>%s</h2> 
    691     <table><tr> 
    692 """ % cat 
    693             for i, (p, w) in enumerate(widgets[cat]): 
    694                 if (i>0) and (i%4 == 0): 
    695                     html += "</tr><tr>\n" 
    696                 wreldir = os.path.relpath(os.path.split(w.filename)[0], wdir)\ 
    697                           if "relpath" in os.path.__dict__ else\ 
    698                           os.path.split(w.filename)[0].replace(wdir, "") 
    699                 docfile = os.path.join(wreldir, 
    700                                        os.path.splitext(os.path.split(w.filename)[1][2:])[0] + ".htm") 
    701                  
    702                 iconfile = os.path.join(wreldir, w.icon) 
    703                 if not os.path.isfile(os.path.join(wdir, iconfile)): 
    704                     iconfile = "icons/Unknown.png" 
    705                 if os.path.isfile(os.path.join(self.directory_documentation(), 
    706                                                "widgets", docfile)): 
    707                     html += """<td id="cid%d" class="left" 
    708       onmouseover="this.style.backgroundColor='#fff7df'" 
    709       onmouseout="this.style.backgroundColor=null" 
    710       onclick="this.style.backgroundColor=null; window.location='%s'"> 
    711       <div class="rnd11"></div> 
    712       <img style="z-index:2" src="%s" title="Widget: Text File" width="28" height="28" /><img style="margin-left:-28px; z-index:1" src="icons/background_32.png" width="28" height="28" /> 
    713       <div class="rnd11"></div> 
    714   </td> 
    715  
    716   <td class="right" 
    717     onmouseover="setElColors(this, %d, '#fff7df')" 
    718     onmouseout="setElColors(this, %d, null)" 
    719     onclick="setElColors(this, %d, null); window.location='%s'"> 
    720       %s 
    721 </td> 
    722 """ % (i, docfile, iconfile, i, i, i, docfile, w.name) 
    723                 else: 
    724                     skeleton_filename = os.path.join(self.directory_documentation(), 
    725                                                      "widgets", 
    726                                                      docfile+".skeleton") 
    727                     if not os.path.isdir(os.path.dirname(skeleton_filename)): 
    728                         os.mkdir(os.path.dirname(skeleton_filename)) 
    729                     open(skeleton_filename, 'w').write(self.widget_doc_skeleton(w, prototype=p)) 
    730                     html += """  <td id="cid%d" class="left-nodoc"> 
    731       <div class="rnd11"></div> 
    732       <img style="z-index:2" src="%s" title="Widget: Text File" width="28" height="28" /><img style="margin-left:-28px; z-index:1" src="icons/background_32.png" width="28" height="28" /> 
    733       <div class="rnd11"></div> 
    734   </td> 
    735   <td class="right-nodoc"> 
    736       <div class="rnd1l"></div> 
    737       %s 
    738       <div class="rnd1l"></div> 
    739  
    740   </td> 
    741 """ % (i, iconfile, w.name) 
    742             html += '</tr></table>\n<div class="rnd2"></div>\n<div class="rnd1"></div>\n</div>\n' 
    743         return html 
    744     ########################################################################### 
    745     # Here end the ugly HTML generators. Only beautiful code from now on! ;) # 
    746     ########################################################################### 
    747          
    748  
    749 class OrangeAddOn(): 
    750     """ 
    751     Stores data about an add-on for Orange.  
    752  
    753     .. attribute:: id 
    754      
    755        ID of the add-on. IDs of registered add-ons are in form 
    756        "registered:<dir>", where <dir> is the directory of add-on's files. 
    757      
    758     .. attribute:: name 
    759         
    760        name of the add-on. 
    761         
    762     .. attribute:: architecture 
    763      
    764        add-on structure version; currently it must have a value of 1. 
    765      
    766     .. attribute:: homepage 
    767      
    768        URL of add-on's web site. 
    769         
    770     .. attribute:: version_str 
    771         
    772        string representation of add-on's version; must be a period-separated 
    773        list of integers. 
    774         
    775     .. attribute:: version 
    776      
    777        parsed value of the :obj:`version_str` attribute - a list of integers. 
    778      
    779     .. attribute:: description 
    780      
    781        textual description of the add-on. 
    782         
    783     .. attribute:: tags 
    784      
    785        textual tags that describe the add-on - a list of strings. 
    786      
    787     .. attribute:: author_organizations 
    788      
    789        a list of strings with names of organizations that developed the add-on. 
    790  
    791     .. attribute:: author_creators 
    792      
    793        a list of strings with names of individuals (persons) that developed the 
    794        add-on. 
    795  
    796     .. attribute:: author_contributors 
    797      
    798        a list of strings with names of organizations and individuals (persons) 
    799        that have made minor contributions to the add-on. 
    800      
    801     .. attribute:: preferred_directory 
    802      
    803        preferred name of the subdirectory under which the add-on is to be 
    804        installed. It is not guaranteed this directory name will be used; for 
    805        example, when such a directory already exists, another name will be 
    806        generated during installation. 
    807     """ 
    808  
    809     def __init__(self, xmlfile=None): 
    810         """ 
    811         Initialize an empty add-on descriptor. Initializes attributes with data 
    812         from an optionally passed XML add-on descriptor; otherwise sets all 
    813         attributes to None or, in case of list attributes, an empty list. 
    814          
    815         :param xmlfile: an optional file name or an instance of minidom's 
    816             Element with XML add-on descriptor. 
    817         :type xmlfile: :class:`xml.dom.minidom.Element` or str or 
    818             :class:`NoneType` 
    819         """ 
    820         self.name = None 
    821         self.architecture = None 
    822         self.homepage = None 
    823         self.id = None 
    824         self.version_str = None 
    825         self.version = None 
    826          
    827         self.description = None 
    828         self.tags = [] 
    829         self.author_organizations = [] 
    830         self.author_creators = [] 
    831         self.author_contributors = [] 
    832          
    833         self.preferred_directory = None 
    834          
    835         self.widgets = []  # List of widgetparser.WidgetMetaData objects 
    836          
    837         if xmlfile: 
    838             xml_doc_root = xmlfile if xmlfile.__class__ is xml.dom.minidom.Element else\ 
    839                          xml.dom.minidom.parse(xmlfile).documentElement 
    840             try: 
    841                 self.parsexml(xml_doc_root) 
    842             finally: 
    843                 xml_doc_root.unlink() 
    844  
    845     def clone(self, new=None): 
    846         """ 
    847         Clone the add-on descriptor, effectively making a deep copy. 
    848          
    849         :param new: a new instance of this class into which to copy the values 
    850             of attributes; if None, a new instance is constructed. 
    851         :type new: :class:`OrangeAddOn` or :class:`NoneType` 
    852         """ 
    853         if not new: 
    854             new = OrangeAddOn() 
    855         new.name = self.name 
    856         new.architecture = self.architecture 
    857         new.homepage = self.homepage 
    858         new.id = self.id 
    859         new.version_str = self.version_str 
    860         new.version = list(self.version) 
    861         new.description = self.description 
    862         new.tags = list(self.tags) 
    863         new.author_organizations = list(self.author_organizations) 
    864         new.author_creator = list(self.author_creators) 
    865         new.author_contributors = list(self.author_contributors) 
    866         new.prefferedDirectory = self.preferred_directory 
    867         new.widgets = [w.clone() for w in self.widgets] 
    868         return new 
    869  
    870     def directory_documentation(self): 
    871         """ 
    872         Return the documentation directory -- the "doc" directory under the 
    873         add-on's directory. 
    874         """ 
    875         #TODO This might be redefined in orngConfiguration. 
    876         return os.path.join(self.directory, "doc") 
    877  
    878     def parsexml(self, root): 
    879         """ 
    880         Parse the add-on's XML descriptor and set object's attributes 
    881         accordingly. 
    882          
    883         :param root: root of the add-on's descriptor (the node with tag name 
    884             "OrangeAddOn"). 
    885         :type root: :class:`xml.dom.minidom.Element` 
    886         """ 
    887         if root.tagName != "OrangeAddOn": 
    888             raise Exception("Invalid XML add-on descriptor: wrong root element name!") 
    889          
    890         mandatory = ["id", "architecture", "name", "version", "meta"] 
    891         textnodes = {"id": "id", "architecture": "architecture", "name": "name", 
    892                      "version": "version_str",  
    893                      "preferredDirectory": "preferredDirectory", 
    894                      "homePage": "homepage"} 
    895         for node in [n for n in root.childNodes if n.nodeType==n.ELEMENT_NODE]: 
    896             if node.tagName in mandatory: 
    897                 mandatory.remove(node.tagName) 
    898                  
    899             if node.tagName in textnodes: 
    900                 setattr(self, textnodes[node.tagName], 
    901                         widgetparser.xml_text_of(node)) 
    902             elif node.tagName == "meta": 
    903                 for node in [n for n in node.childNodes 
    904                              if n.nodeType==n.ELEMENT_NODE]: 
    905                     if node.tagName == "description": 
    906                         self.description = widgetparser.xml_text_of(node, True) 
    907                     elif node.tagName == "tags": 
    908                         for tagNode in [n for n in node.childNodes 
    909                                         if n.nodeType==n.ELEMENT_NODE and 
    910                                         n.tagName == "tag"]: 
    911                             self.tags.append(widgetparser.xml_text_of(tagNode)) 
    912                     elif node.tagName == "authors": 
    913                         authorTypes = {"organization": self.author_organizations, 
    914                                        "creator": self.author_creators, 
    915                                        "contributor": self.author_contributors} 
    916                         for authorNode in [n for n in node.childNodes 
    917                                            if n.nodeType==n.ELEMENT_NODE and 
    918                                            n.tagName in authorTypes]: 
    919                             authorTypes[authorNode.tagName].append(widgetparser.xml_text_of(authorNode)) 
    920             elif node.tagName == "widgets": 
    921                 for node in [n for n in node.childNodes 
    922                              if n.nodeType==n.ELEMENT_NODE]: 
    923                     if node.tagName == "widget": 
    924                         self.widgets.append(widgetparser.WidgetMetaData(node)) 
    925          
    926         if "afterparse" in self.__class__.__dict__: 
    927             self.afterparse(root) 
    928          
    929         self.validate_architecture() 
    930         if mandatory: 
    931             raise Exception("Mandatory elements missing: "+", ".join(mandatory)) 
    932         self.validate_id() 
    933         self.validate_name() 
    934         self.validate_version() 
    935         self.validate_description() 
    936         if self.preferred_directory==None: 
    937             self.preferred_directory = self.name 
    938  
    939     def validate_architecture(self): 
    940         """ 
    941         Raise an exception if the :obj:`architecture` (structure of the add-on) 
    942         is not supported. Currently, only architecture 1 exists. 
    943         """ 
    944         if self.architecture != "1": 
    945             raise Exception("Only architecture '1' is supported by current Orange!") 
    946      
    947     def validate_id(self): 
    948         """ 
    949         Raise an exception if the :obj:`id` is not a valid GUID. 
    950         """ 
    951         idPattern = re.compile("[0-9a-z]{8}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{12}") 
    952         if not idPattern.match(self.id): 
    953             raise Exception("Invalid ID!") 
    954  
    955     def validate_name(self): 
    956         """ 
    957         Raise an exception if the :obj:`name` is empty (or contains only 
    958         whitespace). 
    959         """ 
    960         if self.name.strip() == "": 
    961             raise Exception("Name is a mandatory field!") 
    962      
    963     def validate_version(self): 
    964         """ 
    965         Parse the :obj:`version_str` and populate the :obj:`version` attribute. 
    966         Raise an exception if the version is not in correct format (ie. a 
    967         period-separated list of integers). 
    968         """ 
    969         self.version = []   
    970         for sub in self.version_str.split("."): 
    971             try: 
    972                 self.version.append(int(sub)) 
    973             except: 
    974                 self.version = [] 
    975                 raise Exception("Invalid version string: '%s' is not an integer!" % sub) 
    976         self.version_str = ".".join(map(str,self.version)) 
    977              
    978     def validate_description(self): 
    979         """ 
    980         Raise an exception if the :obj:`description` is empty (or contains only 
    981         whitespace). 
    982         """ 
    983         if self.name.strip() == "": 
    984             raise Exception("Description is a mandatory field!") 
    985          
    986     def has_single_widget(self): 
    987         """ 
    988         Determine whether the add-on contains less than two widgets. 
    989         """ 
    990         return len(self.widgets) < 2 
    991          
    992  
    993 class OrangeAddOnInRepo(OrangeAddOn): 
    994     """ 
    995     Stores data about an add-on for Orange that exists in a repository. 
    996     Additional attributes are: 
    997      
    998     .. attribute:: repository 
    999      
    1000     A repository object (instance of :class:`OrangeAddOnRepository`) that 
    1001     contains data about the add-on's repository. 
    1002  
    1003     .. attribute:: filename 
    1004      
    1005     The name of .oao file in repository. 
    1006      
    1007     """ 
    1008       
    1009     def __init__(self, repository, filename=None, xmlfile=None): 
    1010         """ 
    1011         Constructor only sets the attributes. 
    1012          
    1013         :param repository: the repository that contains the add-on. 
    1014         :type repostitory: :class:`OrangeAddOnRepository` 
    1015          
    1016         :param filename: name of the .oao file in repository (is used only if 
    1017             the XML file does not specify the filename). 
    1018         :type filename: str 
    1019          
    1020         :param xmlfile: an optional file name or an instance of minidom's 
    1021             Element with XML add-on descriptor. 
    1022         :type xmlfile: :class:`xml.dom.minidom.Element` or str or 
    1023             :class:`NoneType` 
    1024         """ 
    1025         OrangeAddOn.__init__(self, xmlfile) 
    1026         self.repository = repository 
    1027         if "filename" not in self.__dict__: 
    1028             self.filename = filename 
    1029      
    1030     def afterparse(self, xml_root):  # Called by OrangeAddOn.parsexml() 
    1031         """ 
    1032         Read the filename attribute from the XML. This method is called by 
    1033         :obj:`OrangeAddOn.parsexml`. 
    1034         """ 
    1035         if xml_root.hasAttribute("filename"): 
    1036             self.filename = xml_root.getAttribute("filename") 
    1037              
    1038     def clone(self, new=None): 
    1039         """ 
    1040         Clone the add-on descriptor, effectively making a deep copy. 
    1041          
    1042         :param new: a new instance of this class into which to copy the values 
    1043             of attributes; if None, a new instance is constructed. 
    1044         :type new: :class:`OrangeAddOn` or :class:`NoneType` 
    1045         """ 
    1046         if not new: 
    1047             new = OrangeAddOnInRepo(self.repository) 
    1048         new.filename = self.filename 
    1049         return OrangeAddOn.clone(self, new) 
    1050  
    1051 class OrangeAddOnInstalled(OrangeAddOn): 
    1052     """ 
    1053     Stores data about an add-on for Orange that has been installed from a 
    1054     repository. Additional attribute is: 
    1055      
    1056     .. attribute:: directory 
    1057      
    1058     Directory of add-on's files. 
    1059      
    1060     """ 
    1061     def __init__(self, directory): 
    1062         """ 
    1063         Constructor only sets the attributes. 
    1064          
    1065         :param directory: directory of add-on's files, including an XML 
    1066             descriptor to read. 
    1067         :type directory: str 
    1068         """ 
    1069         OrangeAddOn.__init__(self, os.path.join(directory, "addon.xml") 
    1070                              if directory else None) 
    1071         self.directory = directory 
    1072      
    1073     def uninstall(self, refresh=True): 
    1074         """ 
    1075         Uninstall the installed add-on. WARNING: all files in add-on's directory 
    1076         are deleted! 
    1077          
    1078         :param refresh:  determines whether add-on list change callback 
    1079             functions are to be called after the unregistration process. This 
    1080             should always be True, except when multiple operations are executed 
    1081             in a batch. 
    1082         :type refresh: boolean 
    1083         """ 
    1084         try: 
    1085             _deltree(self.directory) 
    1086             del installed_addons[self.id] 
    1087             if refresh: 
    1088                 refresh_addons() 
    1089             return True 
    1090         except Exception, e: 
    1091             raise InstallationException("Unable to remove add-on: %s" % 
    1092                                         (self.name, e)) 
    1093          
    1094     def clone(self, new=None): 
    1095         """ 
    1096         Clone the add-on descriptor, effectively making a deep copy. 
    1097          
    1098         :param new: a new instance of this class into which to copy the values 
    1099             of attributes; if None, a new instance is constructed. 
    1100         :type new: :class:`OrangeAddOn` or :class:`NoneType` 
    1101         """ 
    1102         if not new: 
    1103             new = OrangeAddOnInstalled(None) 
    1104         new.directory = self.directory 
    1105         return OrangeAddOn.clone(self, new) 
    1106          
    1107 available_addons = {}  # RepositoryURL -> OrangeAddOnRepository object  
    1108 installed_addons = {}  # ID -> OrangeAddOnInstalled object 
    1109 registered_addons = [] # OrangeRegisteredAddOn objects 
    1110  
    1111 class RepositoryException(Exception): 
    1112     """ 
    1113     An exception that occurs during access to repository location. Behaves 
    1114     exactly as :class:`Exception`. 
    1115  
    1116     """ 
    1117     pass 
    1118  
    1119 global index_re 
    1120 index_re = "[^a-z0-9-']"  # RE for splitting entries in the search index 
    1121  
    1122 class OrangeAddOnRepository: 
    1123     """ 
    1124     Repository of Orange add-ons. 
    1125      
    1126     .. attribute:: name 
    1127      
    1128     A local descriptive name for the repository. 
    1129      
    1130     .. attribute:: url 
    1131      
    1132     URL of the repository root; http and file protocols are supported. 
    1133      
    1134     .. attribute:: addons 
    1135      
    1136     A dictionary mapping GUIDs to lists of add-on objects (of class 
    1137     :class:`OrangeAddOnInRepo`). Each GUID is thus mapped to at least one, 
    1138     but possibly more, different versions of add-on. 
    1139      
    1140     .. attribute:: index 
    1141      
    1142     A search index: sorted list of tuples (s, GUID), where such an entry 
    1143     signifies that when searching for a string that s starts with, add-on with 
    1144     the given GUID should be among results. 
    1145      
    1146     .. attribute:: last_refresh_utc 
    1147      
    1148     :obj:`time.time` of the last reloading of add-on list. 
    1149      
    1150     .. attribute:: has_web_script 
    1151      
    1152     A boolean indicating whether this is an http repository that contains the 
    1153     appropriate server-side python script that returns an XML with a list of 
    1154     add-ons. 
    1155      
    1156     """ 
    1157      
    1158     def __init__(self, name, url, load=True, force=False): 
    1159         """ 
    1160         :param name: a local descriptive name for the repository. 
    1161         :type name: str 
    1162          
    1163         :param url: URL of the repository root; http and file protocols are 
    1164             supported. If the protocol is not given, file:// is assumed. 
    1165         :type url: str 
    1166          
    1167         :param load: determines whether the list of repository's add-ons should 
    1168             be loaded immediately. 
    1169         :type load: boolean 
    1170          
    1171         :param force: determines whether loading of repository's add-on list 
    1172             is mandatory, ie. if an exception is to be raised in case of 
    1173             connection failure. 
    1174         :type force: boolean 
    1175         """ 
    1176          
    1177         self.name = name 
    1178         self.url = url 
    1179         self.checkurl() 
    1180         self.addons = {} 
    1181         self.index = [] 
    1182         self.last_refresh_utc = 0 
    1183         self._refresh_index() 
    1184         self.has_web_script = False 
    1185         if load: 
    1186             try: 
    1187                 self.refreshdata(True, True) 
    1188             except Exception, e: 
    1189                 if force: 
    1190                     warnings.warn("Couldn't load data from repository '%s': %s" 
    1191                                   % (self.name, e), Warning, 0) 
    1192                     return 
    1193                 raise e 
    1194          
    1195     def clone(self, new=None): 
    1196         """ 
    1197         Clone the repository descriptor, effectively making a deep copy. 
    1198          
    1199         :param new: a new instance of this class into which to copy the values 
    1200             of attributes; if None, a new instance is constructed. 
    1201         :type new: :class:`OrangeAddOnRepository` or :class:`NoneType` 
    1202         """ 
    1203         if not new: 
    1204             new = OrangeAddOnRepository(self.name, self.url, load=False) 
    1205         new.addons = {} 
    1206         for (id, versions) in self.addons.items(): 
    1207             new.addons[id] = [ao.clone() for ao in versions] 
    1208         new.index = list(self.index) 
    1209         new.last_refresh_utc = self.last_refresh_utc 
    1210         new.has_web_script = self.has_web_script if hasattr(self, 'has_web_script') else False 
    1211         return new 
    1212  
    1213     def checkurl(self): 
    1214         """ 
    1215         Check the URL for validity. Return True if it begins with "file://" or 
    1216         "http://" or if it does not specify a protocol (in this case, file:// is 
    1217         assumed). 
    1218         """ 
    1219         supportedProtocols = ["file", "http"] 
    1220         if "://" not in self.url: 
    1221             self.url = "file://"+self.url 
    1222         protocol = self.url.split("://")[0] 
    1223         if protocol not in supportedProtocols: 
    1224             raise Exception("Unable to load repository data: protocol '%s' not supported!" % 
    1225                             protocol) 
    1226  
    1227     def _add_addon(self, addon): 
    1228         """ 
    1229         Add the given addon descriptor to the :obj:`addons` dictionary. 
    1230         Operation is sucessful only if there is no add-on with equal GUID 
    1231         (:obj:`OrangeAddOn.id`) and version 
    1232         (:obj:`OrangeAddOn.version`) already in this repository. 
    1233          
    1234         :param addon: add-on descriptor to add. 
    1235         :type addon: :class:`OrangeAddOnInRepo` 
    1236         """ 
    1237         if addon.id in self.addons: 
    1238             versions = self.addons[addon.id] 
    1239             for version in versions: 
    1240                 if version.version == addon.version: 
    1241                     warnings.warn("Ignoring the second occurence of addon '%s'"+ 
    1242                                   ", version '%s'." % (addon.name, 
    1243                                                        addon.version_str), 
    1244                                   Warning, 0) 
    1245                     return 
    1246             versions.append(addon) 
    1247         else: 
    1248             self.addons[addon.id] = [addon] 
    1249  
    1250     def _add_packed_addon(self, oaofile, filename=None): 
    1251         """ 
    1252         Given a local path to an .oao file, add the addon descriptor to the 
    1253         :obj:`addons` dictionary. Specifically, "addon.xml" manifest is unpacked 
    1254         from the .oao, an :class:`OrangeAddOnInRepo` instance is constructed 
    1255         and :obj:`_add_addon` is invoked. 
    1256          
    1257         :param oaofile: path to the .oao file. 
    1258         :type oaofile: str 
    1259          
    1260         :param filename: name of the .oao file within the repository. 
    1261         :type filename: str 
    1262         """ 
    1263         pack = ZipFile(oaofile, 'r') 
    1264         try: 
    1265             manifestfile = _zip_open(pack, 'addon.xml') 
    1266             manifest = xml.dom.minidom.parse(manifestfile).documentElement 
    1267             manifest.appendChild(widgetparser.widgets_xml(pack)) 
    1268             addon = OrangeAddOnInRepo(self, filename, xmlfile=manifest) 
    1269             self._add_addon(addon) 
    1270         except Exception, e: 
    1271             raise Exception("Unable to load add-on descriptor: %s" % e) 
    1272      
    1273     def refreshdata(self, force=False, firstload=False, interval=3600*24): 
    1274         """ 
    1275         Refresh the add-on list if necessary. For an http repository, the 
    1276         server-side python script is invoked. If that fails, or if the 
    1277         repository is on local filesystem (file://), all .oao files are 
    1278         downloaded, unpacked and their manifests (addon.xml) are parsed. 
    1279          
    1280         :param force: force a refresh, even if less than a preset amount of 
    1281             time (see parameter :obj:`interval`) has passed since last refresh 
    1282             (see attribute :obj:`last_refresh_utc`). 
    1283         :type force: boolean 
    1284          
    1285         :param firstload: determines, whether this is the first loading of 
    1286             repository's contents. Right now, the only difference is that when 
    1287             there is no server-side repository script on an http repository and 
    1288             there are also no .oao files, this results in an exception if 
    1289             this parameter is set to True, and in a warning otherwise. 
    1290         :type firstload: boolean 
    1291          
    1292         :parameter interval: an amount of time in seconds that must pass since 
    1293             last refresh (:obj:`last_refresh_utc`) to make the refresh happen. 
    1294         :type interval: int 
    1295         """ 
    1296         if force or (self.last_refresh_utc < time.time() - interval): 
    1297             self.last_refresh_utc = time.time() 
    1298             self.has_web_script = False 
    1299             try: 
    1300                 protocol = self.url.split("://")[0] 
    1301                 if protocol == "http": # A remote repository 
    1302                     # Try to invoke a server-side script to retrieve add-on index (and therefore avoid downloading archives) 
    1303                     repositoryXmlDoc = None 
    1304                     try: 
    1305                         repositoryXmlDoc = urllib2.urlopen(self.url+"/addOnServer.py?machine=1") 
    1306                         repositoryXml = xml.dom.minidom.parse(repositoryXmlDoc).documentElement 
    1307                         if repositoryXml.tagName != "OrangeAddOnRepository": 
    1308                             raise Exception("Invalid XML add-on repository descriptor: wrong root element name!") 
    1309                         self.addons = {} 
    1310                         for (i, node) in enumerate([n for n 
    1311                                                     in repositoryXml.childNodes 
    1312                                                     if n.nodeType==n.ELEMENT_NODE]): 
    1313                             if node.tagName == "OrangeAddOn": 
    1314                                 try: 
    1315                                     addon = OrangeAddOnInRepo(self, xmlfile=node) 
    1316                                     self._add_addon(addon) 
    1317                                 except Exception, e: 
    1318                                     warnings.warn("Ignoring node nr. %d in "+ 
    1319                                                   "repository '%s' because of"+ 
    1320                                                   " an error: %s" % (i+1, 
    1321                                                                      self.name, 
    1322                                                                      e), 
    1323                                                   Warning, 0) 
    1324                         self.has_web_script = True 
    1325                         return True 
    1326                     except Exception, e: 
    1327                         warnings.warn("A problem occurred using server-side script on repository '%s': %s.\nAll add-ons need to be downloaded for their metadata to be extracted!" 
    1328                                       % (self.name, str(e)), Warning, 0) 
    1329  
    1330                     # Invoking script failed - trying to get and parse a directory listing 
    1331                     try: 
    1332                         repoconn = urllib2.urlopen(self.url+'abc') 
    1333                         response = "".join(repoconn.readlines()) 
    1334                     except Exception, e: 
    1335                         raise RepositoryException("Unable to load repository data: %s" % e) 
    1336                     addOnFiles = map(lambda x: x.split('"')[1], 
    1337                                      re.findall(r'href\s*=\s*"[^"/?]*\.oao"', 
    1338                                                 response)) 
    1339                     if len(addOnFiles)==0: 
    1340                         if firstload: 
    1341                             raise RepositoryException("Unable to load reposito"+ 
    1342                                                       "ry data: this is not an"+ 
    1343                                                       " Orange add-on "+ 
    1344                                                       "repository!") 
    1345                         else: 
    1346                             warnings.warn("Repository '%s' is empty ..." % 
    1347                                           self.name, Warning, 0) 
    1348                     self.addons = {} 
    1349                     for addOnFile in addOnFiles: 
    1350                         try: 
    1351                             addOnTmpFile = urllib.urlretrieve(self.url+"/"+addOnFile)[0] 
    1352                             self._add_packed_addon(addOnTmpFile, addOnFile) 
    1353                         except Exception, e: 
    1354                             warnings.warn("Ignoring '%s' in repository '%s' "+ 
    1355                                           "because of an error: %s" % 
    1356                                           (addOnFile, self.name, e), 
    1357                                           Warning, 0) 
    1358                 elif protocol == "file": # A local repository: open each and every archive to obtain data 
    1359                     dir = self.url.replace("file://","") 
    1360                     if not os.path.isdir(dir): 
    1361                         raise RepositoryException("Repository '%s' is not valid: '%s' is not a directory." % (self.name, dir)) 
    1362                     self.addons = {} 
    1363                     for addOnFile in glob.glob(os.path.join(dir, "*.oao")): 
    1364                         try: 
    1365                             self._add_packed_addon(addOnFile, 
    1366                                                   os.path.split(addOnFile)[1]) 
    1367                         except Exception, e: 
    1368                             warnings.warn("Ignoring '%s' in repository '%s' "+ 
    1369                                           "because of an error: %s" % 
    1370                                           (addOnFile, self.name, e), 
    1371                                           Warning, 0) 
    1372                 return True 
    1373             finally: 
    1374                 self._refresh_index() 
    1375         return False 
    1376          
    1377     def _add_to_index(self, addon, text): 
    1378         """ 
    1379         Add the words, found in given text, to the search index, to be 
    1380         associated with given add-on. 
    1381          
    1382         :param addon: add-on to add to the search index. 
    1383         :type addon: :class:`OrangeAddOnInRepo` 
    1384          
    1385         :param text: text from which to extract words to be added to the index. 
    1386         :type text: str 
    1387         """ 
    1388         words = [word for word in re.split(index_re, text.lower()) 
    1389                  if len(word)>1] 
    1390         for word in words: 
    1391             bisect.insort_right(self.index, (word, addon.id) ) 
    1392                  
    1393     def _refresh_index(self): 
    1394         """ 
    1395         Rebuild the search index. 
    1396         """ 
    1397         self.index = [] 
    1398         for addOnVersions in self.addons.values(): 
    1399             for addOn in addOnVersions: 
    1400                 for str in [addOn.name, addOn.description] + addOn.author_creators + addOn.author_contributors + addOn.author_organizations + addOn.tags +\ 
    1401                            [" ".join([w.name, w.contact, w.description, w.category, w.tags]) for w in addOn.widgets]: 
    1402                     self._add_to_index(addOn, str) 
    1403         self.last_search_phrase = None 
    1404         self.last_search_result = None 
    1405                      
    1406     def search_index(self, phrase): 
    1407         """ 
    1408         Search the word index for the given phrase and return a list of 
    1409         matching add-ons' GUIDs. The given phrase is split into sequences 
    1410         of alphanumeric characters, just like strings are split when 
    1411         building the index, and resulting add-ons match all of the words in 
    1412         the phrase. 
    1413          
    1414         :param phrase: a phrase to search. 
    1415         :type phrase: str 
    1416         """ 
    1417         if phrase == self.last_search_phrase: 
    1418             return self.last_search_result 
    1419          
    1420         words = [word for word in re.split(index_re, phrase.lower()) if word!=""] 
    1421         result = set(self.addons.keys()) 
    1422         for word in words: 
    1423             subset = set() 
    1424             i = bisect.bisect_left(self.index, (word, "")) 
    1425             while self.index[i][0][:len(word)] == word: 
    1426                 subset.add(self.index[i][1]) 
    1427                 i += 1 
    1428                 if i>= len(self.index): break 
    1429             result = result.intersection(subset) 
    1430         self.last_search_phrase = phrase 
    1431         self.last_search_result = result 
    1432         return result 
    1433          
    1434 class OrangeDefaultAddOnRepository(OrangeAddOnRepository): 
    1435     """ 
    1436     Repository of Orange add-ons that is added by default. 
    1437      
    1438     It has a hard-coded name of "Default Orange Repository (orange.biolab.si)" 
    1439     and URL "http://orange.biolab.si/add-ons/"; those arguments cannot be 
    1440     passed to the constructor. Also, the :obj:`force` parameter is set to 
    1441     :obj:`True`. Other parameters are passed to the superclass' constructor. 
    1442     """ 
    1443      
    1444     def __init__(self, **args): 
    1445         OrangeAddOnRepository.__init__(self, "Default Orange Repository (orange.biolab.si)", 
    1446                                        "http://orange.biolab.si/add-ons/", 
    1447                                        force=True, **args) 
    1448          
    1449     def clone(self, new=None): 
    1450         if not new: 
    1451             new = OrangeDefaultAddOnRepository(load=False) 
    1452         new.name = self.name 
    1453         new.url = self.url 
    1454         return OrangeAddOnRepository.clone(self, new) 
    1455          
    1456 def load_installed_addons_from_dir(dir): 
    1457     """ 
    1458     Populate the :obj:`installed_addons` dictionary with add-ons, installed 
    1459     into direct subdirectories of the given directory. 
    1460      
    1461     :param dir: directory to search for add-ons. 
    1462     :type dir: str 
    1463     """ 
    1464     if os.path.isdir(dir): 
    1465         for name in os.listdir(dir): 
    1466             addOnDir = os.path.join(dir, name) 
    1467             if not os.path.isdir(addOnDir) or name.startswith("."): 
    1468                 continue 
    1469             try: 
    1470                 addOn = OrangeAddOnInstalled(addOnDir) 
    1471             except Exception, e: 
    1472                 warnings.warn("Add-on in directory '%s' has no valid descriptor (addon.xml): %s" % (addOnDir, e), Warning, 0) 
    1473                 continue 
    1474             if addOn.id in installed_addons: 
    1475                 warnings.warn("Add-on in directory '%s' has the same ID as the addon in '%s'!" % (addOnDir, installed_addons[addOn.id].directory), Warning, 0) 
    1476                 continue 
    1477             installed_addons[addOn.id] = addOn 
    1478  
    1479 def repository_list_filename(): 
    1480     """ 
    1481     Return the full filename of pickled add-on repository list. It resides 
    1482     within Orange settings directory.  
    1483     """ 
    1484     orange_settings_dir = os.path.realpath(Orange.utils.environ.orange_settings_dir) 
    1485     list_file_name = os.path.join(orange_settings_dir, "repositoryList.pickle") 
    1486     if not os.path.isfile(list_file_name): 
    1487         # Try to move the config from the old location. 
    1488         try: 
    1489             canvas_settings_dir = os.path.realpath(Orange.utils.environ.canvas_settings_dir) 
    1490             old_list_file_name = os.path.join(canvas_settings_dir, "repositoryList.pickle") 
    1491             shutil.move(old_list_file_name, list_file_name) 
    1492         except: 
    1493             pass 
    1494      
    1495     return list_file_name 
    1496  
    1497 available_repositories = None 
    1498              
    1499 def load_repositories(refresh=True): 
    1500     """ 
    1501     Populate the :obj:`available_repositories` list by reading the pickled 
    1502     repository list and adding the default repository 
    1503     (http://orange.biolab.si/addons) if it is not yet on the list. Optionally, 
    1504     lists of add-ons in repositories are refreshed. 
    1505      
    1506     :param refresh: determines whether the add-on lists of repositories should 
    1507         be refreshed. 
    1508     :type refresh: boolean 
    1509     """ 
    1510     listFileName = repository_list_filename() 
    1511     global available_repositories 
    1512     available_repositories = [] 
    1513     if os.path.isfile(listFileName): 
    1514         try: 
    1515             import cPickle 
    1516             file = open(listFileName, 'rb') 
    1517             available_repositories = [repo.clone() for repo 
    1518                                       in cPickle.load(file)] 
    1519             file.close() 
    1520         except Exception, e: 
    1521             warnings.warn("Unable to load repository list! Error: %s" % e, Warning, 0) 
    1522     try: 
    1523         update_default_repositories(refresh=refresh) 
    1524     except Exception, e: 
    1525         warnings.warn("Unable to refresh default repositories: %s" % (e), Warning, 0) 
    1526  
    1527     if refresh: 
    1528         for r in available_repositories: 
    1529             #TODO: # Should show some progress (and enable cancellation) 
    1530             try: 
    1531                 r.refreshdata(force=False) 
    1532             except Exception, e: 
    1533                 warnings.warn("Unable to refresh repository %s! Error: %s" % (r.name, e), Warning, 0) 
    1534     save_repositories() 
    1535  
    1536 def save_repositories(): 
    1537     """ 
    1538     Save the add-on repository list (:obj:`available_repositories`) to a  
    1539     specific file (see :obj:`repository_list_filename`). 
    1540     """ 
    1541     listFileName = repository_list_filename() 
    1542     try: 
    1543         import cPickle 
    1544         global available_repositories 
    1545         cPickle.dump(available_repositories, open(listFileName, 'wb')) 
    1546     except Exception, e: 
    1547         warnings.warn("Unable to save repository list! Error: %s" % e, Warning, 0) 
    1548      
    1549  
    1550 def update_default_repositories(refresh=True): 
    1551     """ 
    1552     Make sure the appropriate default repository (and no other 
    1553     :class:`OrangeDefaultAddOnRepository`) is in :obj:`available_repositories`. 
    1554     This function is called by :obj:`load_repositories`. 
    1555      
    1556     :param refresh: determines whether the add-on list of added default 
    1557         repository should be refreshed. 
    1558     :type refresh: boolean 
    1559     """ 
    1560     global available_repositories 
    1561     default = [OrangeDefaultAddOnRepository(load=False)] 
    1562     defaultKeys = [(repo.url, repo.name) for repo in default] 
    1563     existingKeys = [(repo.url, repo.name) for repo in available_repositories] 
    1564      
    1565     for i, key in enumerate(defaultKeys): 
    1566         if key not in existingKeys: 
    1567             available_repositories.append(default[i]) 
    1568             if refresh: 
    1569                 default[i].refreshdata(firstload=True) 
    1570      
    1571     to_remove = [] 
    1572     for i, key in enumerate(existingKeys): 
    1573         if isinstance(available_repositories[i], OrangeDefaultAddOnRepository) and \ 
    1574            key not in defaultKeys: 
    1575             to_remove.append(available_repositories[i]) 
    1576     for tr in to_remove: 
    1577         available_repositories.remove(tr) 
    1578  
    1579 addon_directories = [] 
    1580 def add_addon_directories_to_path(): 
    1581     """ 
    1582     Add directories, related to installed add-ons, to python path, if they are 
    1583     not yet there. Added directories are also stored into 
    1584     :obj:`addon_directories`. If this function is called more than once, the 
    1585     non-first invocation first removes the entries in :obj:`addon_directories` 
    1586     from the path. 
    1587      
    1588     If an add-on is installed in directory D, those directories are added to 
    1589     python path (:obj:`sys.path`): 
    1590     
    1591       - D, 
    1592       - D/widgets 
    1593       - D/widgets/prototypes 
    1594       - D/lib-<platform> 
    1595        
    1596    Here, <platform> is a "-"-separated concatenation of :obj:`sys.platform`, 
    1597    result of :obj:`platform.machine` (an empty string is replaced by "x86") and 
    1598    comma-separated first two components of :obj:`sys.version_info`. 
    1599    """ 
    1600     import os, sys 
    1601     global addon_directories, registered_addons 
    1602     sys.path = [dir for dir in sys.path if dir not in addon_directories] 
    1603     for addOn in installed_addons.values() + registered_addons: 
    1604         path = addOn.directory 
    1605         for p in [os.path.join(path, "widgets", "prototypes"), 
    1606                   os.path.join(path, "widgets"), 
    1607                   path, 
    1608                   os.path.join(path, "lib-%s" % "-".join(( sys.platform, "x86" 
    1609                                                            if (platform.machine()=="") 
    1610                                                            else platform.machine(), 
    1611                                                            ".".join(map(str, sys.version_info[:2])) )) )]: 
    1612             if os.path.isdir(p) and not any([Orange.utils.environ.samepath(p, x) 
    1613                                              for x in sys.path]): 
    1614                 if p not in sys.path: 
    1615                     addon_directories.append(p) 
    1616                     sys.path.insert(0, p) 
    1617  
    1618 def _deltree(dirname): 
    1619      if os.path.exists(dirname): 
    1620         for root,dirs,files in os.walk(dirname): 
    1621                 for dir in dirs: 
    1622                         _deltree(os.path.join(root,dir)) 
    1623                 for file in files: 
    1624                         os.remove(os.path.join(root,file))      
    1625         os.rmdir(dirname) 
    1626  
    1627 class InstallationException(Exception): 
    1628     """ 
    1629     An exception that occurs during add-on installation. Behaves exactly as 
    1630     :class:`Exception`. 
    1631  
    1632     """ 
    1633     pass 
    1634  
    1635 def install_addon(oaofile, global_install=False, refresh=True): 
    1636     """ 
    1637     Install an add-on from given .oao package. Installation means unpacking the 
    1638     .oao file to an appropriate directory (:obj:`Orange.utils.environ.add_ons_dir_user` or 
    1639     :obj:`Orange.utils.environ.add_ons_dir_sys`, depending on the 
    1640     :obj:`global_install` parameter), creating an 
    1641     :class:`OrangeAddOnInstalled` instance and adding this object into the 
    1642     :obj:`installed_addons` dictionary. 
    1643      
    1644     :param global_install: determines whether the given add-on is to be 
    1645         installed globally, ie. for all users. Administrative privileges on 
    1646         the file system are usually needed for that. 
    1647     :type global_install: boolean 
    1648      
    1649     :param refresh: determines whether add-on list change callback 
    1650         functions are to be called after the installation process. This 
    1651         should always be True, except when multiple operations are executed 
    1652         in a batch. 
    1653     :type refresh: boolean 
    1654     """ 
    1655     try: 
    1656         pack = ZipFile(oaofile, 'r') 
    1657     except Exception, e: 
    1658         raise Exception("Unable to unpack the add-on '%s': %s" % (oaofile, e)) 
    1659          
    1660     try: 
    1661         for filename in pack.namelist(): 
    1662             if filename[0]=="\\" or filename[0]=="/" or filename[:2]=="..": 
    1663                 raise InstallationException("Refusing to install unsafe package: it contains file named '%s'!" % filename) 
    1664          
    1665         root = Orange.utils.environ.add_ons_dir if global_install else Orange.utils.environ.add_ons_dir_user 
    1666          
    1667         try: 
    1668             manifest = _zip_open(pack, 'addon.xml') 
    1669             addon = OrangeAddOn(manifest) 
    1670         except Exception, e: 
    1671             raise Exception("Unable to load add-on descriptor: %s" % e) 
    1672          
    1673         if addon.id in installed_addons: 
    1674             raise InstallationException("An add-on with this ID is already installed!") 
    1675          
    1676         # Find appropriate directory name for the new add-on. 
    1677         i = 1 
    1678         while True: 
    1679             addon_dir = os.path.join(root, 
    1680                                      addon.preferred_directory + ("" if i<2 else " (%d)"%i)) 
    1681             if not os.path.exists(addon_dir): 
    1682                 break 
    1683             i += 1 
    1684             if i>1000:  # Avoid infinite loop if something goes wrong. 
    1685                 raise InstallationException("Cannot find an appropriate directory name for the new add-on.") 
    1686          
    1687         # Install (unpack) add-on. 
    1688         try: 
    1689             os.makedirs(addon_dir) 
    1690         except OSError, e: 
    1691             if e.errno==13:  # Permission Denied 
    1692                 raise InstallationException("No write permission for the add-ons directory!") 
    1693         except Exception, e: 
    1694                 raise Exception("Cannot create a new add-on directory: %s" % e) 
    1695  
    1696         try: 
    1697             if hasattr(pack, "extractall"): 
    1698                 pack.extractall(addon_dir) 
    1699             else: # Python 2.5 
    1700                 import shutil 
    1701                 for filename in pack.namelist(): 
    1702                     # don't include leading "/" from file name if present 
    1703                     if filename[0] == '/': 
    1704                         targetpath = os.path.join(addon_dir, filename[1:]) 
    1705                     else: 
    1706                         targetpath = os.path.join(addon_dir, filename) 
    1707                     upperdirs = os.path.dirname(targetpath) 
    1708                     if upperdirs and not os.path.exists(upperdirs): 
    1709                         os.makedirs(upperdirs) 
    1710              
    1711                     if filename[-1] == '/': 
    1712                         if not os.path.isdir(targetpath): 
    1713                             os.mkdir(targetpath) 
    1714                         continue 
    1715              
    1716                     source = _zip_open(pack, filename) 
    1717                     target = file(targetpath, "wb") 
    1718                     shutil.copyfileobj(source, target) 
    1719                     source.close() 
    1720                     target.close() 
    1721  
    1722             addon = OrangeAddOnInstalled(addon_dir) 
    1723             installed_addons[addon.id] = addon 
    1724         except Exception, e: 
    1725             try: 
    1726                 _deltree(addon_dir) 
    1727             except: 
    1728                 pass 
    1729             raise Exception("Cannot install add-on: %s"%e) 
    1730          
    1731         if refresh: 
    1732             refresh_addons() 
     218            raise Exception("Unable to install add-on: %s" % e) 
    1733219    finally: 
    1734         pack.close() 
    1735  
    1736 def install_addon_from_repo(addon_in_repo, global_install=False, refresh=True): 
    1737     """ 
    1738     Retrieve the .oao file from the repository, then call :obj:`install_addon` 
    1739     on the resulting file, passing it given parameters. 
    1740      
    1741     :param addon_in_repo: add-on in repository to be installed. 
    1742     :type addon_in_repo: :class:`OrangeAddOnInRepo` 
    1743     """ 
    1744     try: 
    1745         tmpfile = urllib.urlretrieve(addon_in_repo.repository.url+"/"+addon_in_repo.filename)[0] 
    1746     except Exception, e: 
    1747         raise InstallationException("Unable to download add-on from repository: %s" % e) 
    1748     install_addon(tmpfile, global_install, refresh) 
    1749  
    1750 def load_addons(): 
    1751     """ 
    1752     Call :obj:`load_installed_addons_from_dir` on a system-wide add-on 
    1753     installation directory (:obj:`orngEnviron.addOnsDirSys`) and user-specific 
    1754     add-on installation directory (:obj:`orngEnviron.addOnsDirUser`). 
    1755     """ 
    1756     load_installed_addons_from_dir(Orange.utils.environ.add_ons_dir) 
    1757     load_installed_addons_from_dir(Orange.utils.environ.add_ons_dir_user) 
    1758  
    1759 def refresh_addons(reload_path=False): 
    1760     """ 
    1761     Call add-on list change callbacks (ie. functions in 
    1762     :obj:`addon_refresh_callback`) and, optionally, refresh the python path 
    1763     (:obj:`sys.path`) with appropriate add-on directories (ie. call 
    1764     :obj:`addon_refresh_callback`). 
    1765      
    1766     :param reload_path: determines whether python path should be refreshed. 
    1767     :type reload_path: boolean 
    1768     """ 
    1769     if reload_path: 
    1770         add_addon_directories_to_path() 
     220        shutil.rmtree(tmpdir, ignore_errors=True) 
     221 
     222    for p in list(sys.path): 
     223        site.addsitedir(p) 
     224    reload(pkg_resources) 
     225    for p in list(sys.path): 
     226        pkg_resources.find_distributions(p) 
     227    from orngRegistry import load_new_addons 
     228    load_new_addons() 
     229    load_installed_addons() 
    1771230    for func in addon_refresh_callback: 
    1772231        func() 
    1773          
    1774 # Registered add-ons support         
     232 
     233def uninstall(name): #TODO 
     234    raise Exception('Unable to uninstall %s: uninstallation of add-ons is not yet implemented.') 
     235    # Implement this either by using pip.commands.uninstall, and complain if pip is not installed on the system, 
     236    # or by "stealing" pip's uninstallation code. 
     237 
     238def upgrade(name): 
     239    install(name) 
     240 
     241load_installed_addons() 
     242 
     243 
     244 
     245# Support for loading legacy "registered" add-ons 
    1775246def __read_addons_list(addons_file, systemwide): 
    1776247    if os.path.isfile(addons_file): 
    1777         name_path_list = [tuple([x.strip() for x in lne.split("\t")]) 
    1778                           for lne in file(addons_file, "rt")] 
    1779         return [OrangeRegisteredAddOn(name, path, systemwide) 
    1780                 for (name, path) in name_path_list] 
     248        return [tuple([x.strip() for x in lne.split("\t")]) 
     249                for lne in file(addons_file, "rt")] 
    1781250    else: 
    1782251        return [] 
    1783      
    1784 def __read_addon_lists(user_only=False): 
    1785     return __read_addons_list(os.path.join(Orange.utils.environ.orange_settings_dir, "add-ons.txt"), 
    1786                               False) + ([] if user_only else 
    1787                                         __read_addons_list(os.path.join(Orange.utils.environ.install_dir, "add-ons.txt"), 
    1788                                                            True)) 
    1789  
    1790 def __write_addon_lists(addons, user_only=False): 
    1791     file(os.path.join(Orange.utils.environ.orange_settings_dir, "add-ons.txt"), "wt").write("\n".join(["%s\t%s" % (a.name, a.directory) for a in addons if not a.systemwide])) 
    1792     if not user_only: 
    1793         file(os.path.join(Orange.utils.environ.install_dir        , "add-ons.txt"), "wt").write("\n".join(["%s\t%s" % (a.name, a.directory) for a in addons if     a.systemwide])) 
    1794  
    1795 def register_addon(name, path, add = True, refresh=True, systemwide=False): 
    1796     """ 
    1797     Register the given path as an registered add-on with a given descriptive 
    1798     name. The operation is persistent, ie. on next :obj:`load_addons` call the 
    1799     path will still appear as registered. 
    1800      
    1801     :param name: a descriptive name for the registered add-on. 
    1802     :type name: str 
    1803      
    1804     :param path: path to be registered. 
    1805     :type path: str 
    1806      
    1807     :param add: if False, the given path is UNREGISTERED instead of registered. 
    1808     :type add: boolean 
    1809      
    1810     :param refresh: determines whether callbacks should be called after the 
    1811         procedure. 
    1812     :type refresh: boolean 
    1813      
    1814     :param systemwide: determines whether the path is to be registered 
    1815         system-wide, i.e. for all users. Administrative privileges on the 
    1816         filesystem are usually needed for that. 
    1817     :type systemwide: boolean 
    1818     """ 
    1819     if not add: 
    1820         unregister_addon(name, path, user_only=not systemwide) 
    1821     else: 
    1822         if os.path.isfile(path): 
    1823             path = os.path.dirname(path) 
    1824         __write_addon_lists([a for a in __read_addon_lists(user_only=not systemwide) 
    1825                              if a.name != name and a.directory != path] +\ 
    1826                            ([OrangeRegisteredAddOn(name, path, systemwide)] or []), 
    1827                              user_only=not systemwide) 
    1828      
    1829         global registered_addons 
    1830         registered_addons.append( OrangeRegisteredAddOn(name, path, systemwide) ) 
    1831     if refresh: 
    1832         refresh_addons() 
    1833  
    1834 def unregister_addon(name, path, user_only=False): 
    1835     """ 
    1836     Unregister the given path if it has been registered as an add-on with given 
    1837     descriptive name. The operation is persistent, ie. on next 
    1838     :obj:`load_addons` call the path will no longer appear as registered. 
    1839      
    1840     :param name: a descriptive name of the registered add-on to be unregistered. 
    1841     :type name: str 
    1842      
    1843     :param path: path to be unregistered. 
    1844     :type path: str 
    1845  
    1846     :param user_only: determines whether the path to be unregistered is 
    1847         registered for this user only, ie. not system-wide. Administrative 
    1848         privileges on the filesystem are usually needed to unregister a 
    1849         system-wide registered add-on. 
    1850     :type systemwide: boolean 
    1851     """ 
    1852     global registered_addons 
    1853     registered_addons = [ao for ao in registered_addons 
    1854                          if (ao.name!=name) or (ao.directory!=path) or 
    1855                          (user_only and ao.systemwide)] 
    1856     __write_addon_lists([a for a in __read_addon_lists(user_only=user_only) 
    1857                          if a.name != name and a.directory != path], 
    1858                          user_only=user_only) 
    1859  
    1860  
    1861 def __get_registered_addons(): 
    1862     return {'registered_addons': __read_addon_lists()} 
    1863  
    1864 load_addons() 
    1865 globals().update(__get_registered_addons()) 
    1866  
    1867 addon_refresh_callback = [] 
    1868 globals().update({'addon_refresh_callback': addon_refresh_callback}) 
    1869  
    1870 add_addon_directories_to_path() 
    1871  
    1872 load_repositories(refresh=False) 
     252 
     253registered = __read_addons_list(os.path.join(Orange.utils.environ.orange_settings_dir, "add-ons.txt"), False) + \ 
     254             __read_addons_list(os.path.join(Orange.utils.environ.install_dir, "add-ons.txt"), True) 
     255 
     256for name, path in registered: 
     257    for p in [os.path.join(path, "widgets", "prototypes"), 
     258          os.path.join(path, "widgets"), 
     259          path, 
     260          os.path.join(path, "lib-%s" % "-".join(( sys.platform, "x86" if (platform.machine()=="") 
     261          else platform.machine(), ".".join(map(str, sys.version_info[:2])) )) )]: 
     262        if os.path.isdir(p) and not any([Orange.utils.environ.samepath(p, x) 
     263                                         for x in sys.path]): 
     264            if p not in sys.path: 
     265                sys.path.insert(0, p) 
     266 
     267#TODO Show some progress to the user at least during the installation procedure. 
Note: See TracChangeset for help on using the changeset viewer.