Changeset 11018:b7cbf2b86522 in orange for Orange/utils/addons.py


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.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • 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.