source: orange/Orange/utils/addons.py @ 11037:4b41e882f3cb

Revision 11037:4b41e882f3cb, 10.6 KB checked in by Matija Polajnar <matija.polajnar@…>, 17 months ago (diff)

Make add-on names case-insensitive; windows likes to have them all lowercase after installation. Hopefully fixes #1242.

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