source: orange/Orange/utils/addons.py @ 11094:67f486d63faf

Revision 11094:67f486d63faf, 10.9 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Running setup.py script in a subprocess.

If the package uses 'distribute_setup' module the installation fails when
the currently installed (and already imported) version is older then the
requested version.

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