source: orange/Orange/utils/addons.py @ 11653:705107ae06c1

Revision 11653:705107ae06c1, 11.1 KB checked in by Ales Erjavec <ales.erjavec@…>, 8 months ago (diff)

Removed the use of global 'socket.setdefaulttimeout'.

(fixes #1310).

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