source: orange/Orange/utils/addons.py @ 11018:b7cbf2b86522

Revision 11018:b7cbf2b86522, 9.8 KB checked in by Matija Polajnar <matija.polajnar@…>, 17 months ago (diff)

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

Line 
1from __future__ import absolute_import
2"""
3==============================
4Add-on Management (``addons``)
5==============================
6
7.. index:: add-ons
8
9Orange.utils.addons module provides a framework for Orange add-on management. As
10soon as it is imported, the following initialization takes place: the list of
11installed add-ons is loaded, their directories are added to python path
12(:obj:`sys.path`) the callback list is initialized, the stored repository list is
13loaded. The most important consequence of importing the module is thus the
14injection of add-ons into the namespace.
15
16"""
17
18#TODO Document this module.
19
20import socket
21import shelve
22import xmlrpclib
23import warnings
24import re
25import pkg_resources
26import tempfile
27import tarfile
28import shutil
29import os
30import sys
31import platform
32from collections import namedtuple, defaultdict
33
34import Orange.utils.environ
35
36ADDONS_ENTRY_POINT="orange.addons"
37
38socket.setdefaulttimeout(120)  # In seconds.
39
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/')
96    except:
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)
171        try:
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')
206        except Exception, e:
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
212        try:
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)
217        except Exception, e:
218            raise Exception("Unable to install add-on: %s" % e)
219    finally:
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()
230    for func in addon_refresh_callback:
231        func()
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
246def __read_addons_list(addons_file, systemwide):
247    if os.path.isfile(addons_file):
248        return [tuple([x.strip() for x in lne.split("\t")])
249                for lne in file(addons_file, "rt")]
250    else:
251        return []
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 TracBrowser for help on using the repository browser.