source: orange/Orange/utils/addons.py @ 11654:3c47a792586a

Revision 11654:3c47a792586a, 11.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 8 months ago (diff)

Changed the global 'addons_corrupted' flag to a function.

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