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.

RevLine 
[11018]1from __future__ import absolute_import
[8042]2"""
3==============================
4Add-on Management (``addons``)
5==============================
6
7.. index:: add-ons
8
[10581]9Orange.utils.addons module provides a framework for Orange add-on management. As
[8042]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
[11018]12(:obj:`sys.path`) the callback list is initialized, the stored repository list is
[8042]13loaded. The most important consequence of importing the module is thus the
[11018]14injection of add-ons into the namespace.
[8042]15
16"""
17
[11018]18#TODO Document this module.
[8042]19
[11018]20import socket
21import shelve
22import xmlrpclib
23import warnings
[8042]24import re
[11018]25import pkg_resources
26import tempfile
27import tarfile
28import shutil
[8042]29import os
30import sys
31import platform
[11094]32import subprocess
[11653]33import urllib2
34import urlparse
35import posixpath
36import site
[11094]37
[11018]38from collections import namedtuple, defaultdict
[11071]39from contextlib import closing
[8042]40
[10580]41import Orange.utils.environ
[8042]42
[11018]43ADDONS_ENTRY_POINT="orange.addons"
[8042]44
45
[11018]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.
[8042]51
[11018]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")
[11653]55
[11071]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
[11018]66
[11024]67
[11654]68def addons_corrupted():
69    with closing(open_addons()) as addons:
70        return len(addons) == 0
[11597]71
[11018]72addon_refresh_callback = []
73
74global index
75index = defaultdict(list)
76def rebuild_index():
77    global index
78
79    index = defaultdict(list)
[11071]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)
[11018]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:
[11071]96        with closing(open_addons()) as addons:
97            return addons.keys()
[11018]98    for word in words:
99        result.update(index[word])
100    return result
101
[11020]102def refresh_available_addons(force=False, progress_callback=None):
[11018]103    pypi = xmlrpclib.ServerProxy('http://pypi.python.org/pypi')
[11020]104    if progress_callback:
105        progress_callback(1, 0)
[11018]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
[8042]114    try:
[11018]115        import slumber
116        readthedocs = slumber.API(base_url='http://readthedocs.org/api/v1/')
[8042]117    except:
[11018]118        readthedocs = None
[8042]119
[11018]120    docs = {}
[11020]121    if progress_callback:
122        progress_callback(len(pkg_dict)+1, 1)
[11071]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]
[8042]129
[11071]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)
[8042]154
[11018]155    rebuild_index()
[8042]156
[11018]157def load_installed_addons():
158    found = set()
[11071]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)
[11018]182    rebuild_index()
183
[11094]184
[11018]185def run_setup(setup_script, args):
[11094]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)
[11018]207
208
[11021]209def install(name, progress_callback=None):
210    if progress_callback:
211        progress_callback(1, 0)
[11653]212
213    with closing(open_addons()) as addons:
214        addon = addons[name.lower()]
215    release_url = addon.release_url
[11018]216
217    try:
[11653]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')
[8042]236
[11018]237        if not os.path.isfile(setup_py):
[11653]238            raise Exception("Unable to install add-on - it is not properly "
239                            "packed.")
[8042]240
[11094]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)
[11018]245    finally:
246        shutil.rmtree(tmpdir, ignore_errors=True)
[8042]247
[11018]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()
[8042]256    for func in addon_refresh_callback:
257        func()
[11018]258
[11025]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.")
[11018]266
[11021]267def upgrade(name, progress_callback=None):
268    install(name, progress_callback)
[11018]269
270load_installed_addons()
271
272
273
274# Support for loading legacy "registered" add-ons
[8042]275def __read_addons_list(addons_file, systemwide):
276    if os.path.isfile(addons_file):
[11018]277        return [tuple([x.strip() for x in lne.split("\t")])
278                for lne in file(addons_file, "rt")]
[8042]279    else:
280        return []
281
[11018]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)
[8042]284
[11018]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)
[8042]295
[11018]296#TODO Show some progress to the user at least during the installation procedure.
Note: See TracBrowser for help on using the repository browser.