source: orange/Orange/utils/addons.py @ 11071:e0b3d7e7dd61

Revision 11071:e0b3d7e7dd61, 11.2 KB checked in by Matija Polajnar <matija.polajnar@…>, 16 months ago (diff)

addons: always close shelve after use.

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
33from contextlib import closing
34
35import Orange.utils.environ
36
37ADDONS_ENTRY_POINT="orange.addons"
38
39socket.setdefaulttimeout(120)  # In seconds.
40
41OrangeAddOn = namedtuple('OrangeAddOn', ['name', 'available_version', 'installed_version', 'summary', 'description',
42                                         'author', 'docs_url', 'keywords', 'homepage', 'package_url',
43                                         'release_url', 'release_size', 'python_version'])
44#It'd be great if we could somehow read a list and descriptions of widgets, show them in the dialog and enable
45#search of add-ons based on keywords in widget names and descriptions.
46
47INDEX_RE = "[^a-z0-9-']"  # RE for splitting entries in the search index
48
49AOLIST_FILE = os.path.join(Orange.utils.environ.orange_settings_dir, "addons.shelve")
50def open_addons():
51    try:
52        addons = shelve.open(AOLIST_FILE, 'c')
53        if any(name != name.lower() for name, record in addons.items()):  # Try to read the whole list and check for sanity.
54            raise Exception("Corrupted add-on list.")
55    except:
56        if os.path.isfile(AOLIST_FILE):
57            os.remove(AOLIST_FILE)
58        addons = shelve.open(AOLIST_FILE, 'n')
59    return addons
60
61global addons_corrupted
62with closing(open_addons()) as addons:
63    addons_corrupted = len(addons)==0
64
65addon_refresh_callback = []
66
67global index
68index = defaultdict(list)
69def rebuild_index():
70    global index
71
72    index = defaultdict(list)
73    with closing(open_addons()) as addons:
74        for name, ao in addons.items():
75            for s in [name, ao.summary, ao.description, ao.author] + (ao.keywords if ao.keywords else []):
76                if not s:
77                    continue
78                words = [word for word in re.split(INDEX_RE, s.lower())
79                         if len(word)>1]
80                for word in words:
81                    for i in range(len(word)):
82                        index[word[:i+1]].append(name)
83
84def search_index(query):
85    global index
86    result = set()
87    words = [word for word in re.split(INDEX_RE, query.lower()) if len(word)>1]
88    if not words:
89        with closing(open_addons()) as addons:
90            return addons.keys()
91    for word in words:
92        result.update(index[word])
93    return result
94
95def refresh_available_addons(force=False, progress_callback=None):
96    pypi = xmlrpclib.ServerProxy('http://pypi.python.org/pypi')
97    if progress_callback:
98        progress_callback(1, 0)
99
100    pkg_dict = {}
101    for data in pypi.search({'keywords': 'orange'}):
102        name = data['name']
103        order = data['_pypi_ordering']
104        if name not in pkg_dict or pkg_dict[name][0] < order:
105            pkg_dict[name] = (order, data['version'])
106
107    try:
108        import slumber
109        readthedocs = slumber.API(base_url='http://readthedocs.org/api/v1/')
110    except:
111        readthedocs = None
112
113    global addons_corrupted
114    docs = {}
115    if progress_callback:
116        progress_callback(len(pkg_dict)+1, 1)
117    with closing(open_addons()) as addons:
118        for i, (name, (_, version)) in enumerate(pkg_dict.items()):
119            if force or name not in addons or addons[name.lower()].available_version != version:
120                try:
121                    data = pypi.release_data(name, version)
122                    rel = pypi.release_urls(name, version)[0]
123
124                    if readthedocs:
125                        try:
126                            docs = readthedocs.project.get(slug=name.lower())['objects'][0]
127                        except:
128                            docs = {}
129                    addons[name.lower()] = OrangeAddOn(name = name,
130                                               available_version = data['version'],
131                                               installed_version = addons[name.lower()].installed_version if name.lower() in addons else None,
132                                               summary = data['summary'],
133                                               description = data.get('description', ''),
134                                               author = str((data.get('author', '') or '') + ' ' + (data.get('author_email', '') or '')).strip(),
135                                               docs_url = data.get('docs_url', docs.get('subdomain', '')),
136                                               keywords = data.get('keywords', "").split(","),
137                                               homepage = data.get('home_page', ''),
138                                               package_url = data.get('package_url', ''),
139                                               release_url = rel.get('url', None),
140                                               release_size = rel.get('size', -1),
141                                               python_version = rel.get('python_version', None))
142                except Exception, e:
143                    import traceback
144                    traceback.print_exc()
145                    warnings.warn('Could not load data for the following add-on: %s'%name)
146            if progress_callback:
147                progress_callback(len(pkg_dict)+1, i+2)
148        addons_corrupted = False
149
150    rebuild_index()
151
152def load_installed_addons():
153    found = set()
154    with closing(open_addons()) as addons:
155        for entry_point in pkg_resources.iter_entry_points(ADDONS_ENTRY_POINT):
156            name, version = entry_point.dist.project_name, entry_point.dist.version
157            #TODO We could import setup.py from entry_point.location and load descriptions and such ...
158            if name.lower() in addons:
159                addons[name.lower()] = addons[name.lower()]._replace(installed_version = version)
160            else:
161                addons[name.lower()] = OrangeAddOn(name = name,
162                    available_version = None,
163                    installed_version = version,
164                    summary = "",
165                    description = "",
166                    author = "",
167                    docs_url = "",
168                    keywords = "",
169                    homepage = "",
170                    package_url = "",
171                    release_url = "",
172                    release_size = None,
173                    python_version = None)
174            found.add(name.lower())
175        for name in set(addons).difference(found):
176            addons[name.lower()] = addons[name.lower()]._replace(installed_version = None)
177    rebuild_index()
178
179def run_setup(setup_script, args):
180    old_dir = os.getcwd()
181    save_argv = sys.argv[:]
182    save_path = sys.path[:]
183    setup_dir = os.path.abspath(os.path.dirname(setup_script))
184    temp_dir = os.path.join(setup_dir,'temp')
185    if not os.path.isdir(temp_dir): os.makedirs(temp_dir)
186    save_tmp = tempfile.tempdir
187    save_modules = sys.modules.copy()
188    try:
189        tempfile.tempdir = temp_dir
190        os.chdir(setup_dir)
191        try:
192            sys.argv[:] = [setup_script]+list(args)
193            sys.path.insert(0, setup_dir)
194            execfile(
195                    "setup.py",
196                    {'__file__':setup_script, '__name__':'__main__'}
197                )
198        except SystemExit, v:
199            if v.args and v.args[0]:
200                raise
201                # Normal exit, just return
202    finally:
203        sys.modules.update(save_modules)
204        for key in list(sys.modules):
205            if key not in save_modules: del sys.modules[key]
206        os.chdir(old_dir)
207        sys.path[:] = save_path
208        sys.argv[:] = save_argv
209        tempfile.tempdir = save_tmp
210
211
212def install(name, progress_callback=None):
213    if progress_callback:
214        progress_callback(1, 0)
215    import site
216    try:
217        import urllib
218        rh = (lambda done, bs, fs: progress_callback(fs/bs, done)) if progress_callback else None
219        with closing(open_addons()) as addons:
220            egg = urllib.urlretrieve(addons[name.lower()].release_url, reporthook=rh)[0]
221    except Exception, e:
222        raise Exception("Unable to download add-on from repository: %s" % e)
223
224    try:
225        try:
226            tmpdir = tempfile.mkdtemp()
227            egg_contents = tarfile.open(egg)
228            egg_contents.extractall(tmpdir)
229            with closing(open_addons()) as addons:
230                setup_py = os.path.join(tmpdir, name+'-'+addons[name.lower()].available_version, 'setup.py')
231        except Exception, e:
232            raise Exception("Unable to unpack add-on: %s" % e)
233
234        if not os.path.isfile(setup_py):
235            raise Exception("Unable to install add-on - it is not properly packed.")
236
237        try:
238            switches = []
239            if site.USER_SITE in sys.path:   # we're not in a virtualenv
240                switches.append('--user')
241            run_setup(setup_py, ['install'] + switches)
242        except Exception, e:
243            raise Exception("Unable to install add-on: %s" % e)
244    finally:
245        shutil.rmtree(tmpdir, ignore_errors=True)
246
247    for p in list(sys.path):
248        site.addsitedir(p)
249    reload(pkg_resources)
250    for p in list(sys.path):
251        pkg_resources.find_distributions(p)
252    from orngRegistry import load_new_addons
253    load_new_addons()
254    load_installed_addons()
255    for func in addon_refresh_callback:
256        func()
257
258def uninstall(name, progress_callback=None):
259    try:
260        import pip.req
261        ao = pip.req.InstallRequirement(name, None)
262        ao.uninstall(True)
263    except ImportError:
264        raise Exception("Pip is required for add-on uninstallation. Install pip and try again.")
265
266def upgrade(name, progress_callback=None):
267    install(name, progress_callback)
268
269load_installed_addons()
270
271
272
273# Support for loading legacy "registered" add-ons
274def __read_addons_list(addons_file, systemwide):
275    if os.path.isfile(addons_file):
276        return [tuple([x.strip() for x in lne.split("\t")])
277                for lne in file(addons_file, "rt")]
278    else:
279        return []
280
281registered = __read_addons_list(os.path.join(Orange.utils.environ.orange_settings_dir, "add-ons.txt"), False) + \
282             __read_addons_list(os.path.join(Orange.utils.environ.install_dir, "add-ons.txt"), True)
283
284for name, path in registered:
285    for p in [os.path.join(path, "widgets", "prototypes"),
286          os.path.join(path, "widgets"),
287          path,
288          os.path.join(path, "lib-%s" % "-".join(( sys.platform, "x86" if (platform.machine()=="")
289          else platform.machine(), ".".join(map(str, sys.version_info[:2])) )) )]:
290        if os.path.isdir(p) and not any([Orange.utils.environ.samepath(p, x)
291                                         for x in sys.path]):
292            if p not in sys.path:
293                sys.path.insert(0, p)
294
295#TODO Show some progress to the user at least during the installation procedure.
Note: See TracBrowser for help on using the repository browser.