Aleš Erjavec avatar Aleš Erjavec committed 9f8fc6d

Added canvas help search system.

Comments (0)

Files changed (6)

Orange/OrangeCanvas/help/__init__.py

+from .provider import HelpProvider
+from .manager import HelpManager

Orange/OrangeCanvas/help/intersphinx.py

+"""
+Parsers for intersphinx inventory files
+
+Taken from `sphinx.ext.intersphinx`
+
+"""
+
+import re
+import codecs
+import zlib
+
+b = str
+UTF8StreamReader = codecs.lookup('utf-8')[2]
+
+
+def read_inventory_v1(f, uri, join):
+    f = UTF8StreamReader(f)
+    invdata = {}
+    line = f.next()
+    projname = line.rstrip()[11:]
+    line = f.next()
+    version = line.rstrip()[11:]
+    for line in f:
+        name, type, location = line.rstrip().split(None, 2)
+        location = join(uri, location)
+        # version 1 did not add anchors to the location
+        if type == 'mod':
+            type = 'py:module'
+            location += '#module-' + name
+        else:
+            type = 'py:' + type
+            location += '#' + name
+        invdata.setdefault(type, {})[name] = (projname, version, location, '-')
+    return invdata
+
+
+def read_inventory_v2(f, uri, join, bufsize=16*1024):
+    invdata = {}
+    line = f.readline()
+    projname = line.rstrip()[11:].decode('utf-8')
+    line = f.readline()
+    version = line.rstrip()[11:].decode('utf-8')
+    line = f.readline().decode('utf-8')
+    if 'zlib' not in line:
+        raise ValueError
+
+    def read_chunks():
+        decompressor = zlib.decompressobj()
+        for chunk in iter(lambda: f.read(bufsize), b('')):
+            yield decompressor.decompress(chunk)
+        yield decompressor.flush()
+
+    def split_lines(iter):
+        buf = b('')
+        for chunk in iter:
+            buf += chunk
+            lineend = buf.find(b('\n'))
+            while lineend != -1:
+                yield buf[:lineend].decode('utf-8')
+                buf = buf[lineend+1:]
+                lineend = buf.find(b('\n'))
+        assert not buf
+
+    for line in split_lines(read_chunks()):
+        # be careful to handle names with embedded spaces correctly
+        m = re.match(r'(?x)(.+?)\s+(\S*:\S*)\s+(\S+)\s+(\S+)\s+(.*)',
+                     line.rstrip())
+        if not m:
+            continue
+        name, type, prio, location, dispname = m.groups()
+        if location.endswith(u'$'):
+            location = location[:-1] + name
+        location = join(uri, location)
+        invdata.setdefault(type, {})[name] = (projname, version,
+                                              location, dispname)
+    return invdata

Orange/OrangeCanvas/help/manager.py

+"""
+
+"""
+import sys
+import os
+import string
+import itertools
+import logging
+
+from operator import itemgetter
+
+import pkg_resources
+
+from .provider import IntersphinxHelpProvider
+
+from PyQt4.QtCore import QObject, QUrl
+
+log = logging.getLogger(__name__)
+
+
+class HelpManager(QObject):
+    def __init__(self, parent=None):
+        QObject.__init__(self, parent)
+        self._registry = None
+        self._initialized = False
+        self._providers = {}
+
+    def set_registry(self, registry):
+        """
+        Set the widget registry for which the manager should
+        provide help.
+
+        """
+        if self._registry is not registry:
+            self._registry = registry
+            self._initialized = False
+            self.initialize()
+
+    def registry(self):
+        """
+        Return the previously set with set_registry.
+        """
+        return self._registry
+
+    def initialize(self):
+        if self._initialized:
+            return
+
+        reg = self._registry
+        all_projects = set(desc.project_name for desc in reg.widgets())
+
+        providers = []
+        for project in set(all_projects) - set(self._providers.keys()):
+            provider = None
+            try:
+                dist = pkg_resources.get_distribution(project)
+                provider = get_help_provider_for_distribution(dist)
+            except Exception:
+                log.exception("Error while initializing help "
+                              "provider for %r", desc.project_name)
+
+            if provider:
+                providers.append((project, provider))
+                provider.setParent(self)
+
+        self._providers.update(dict(providers))
+        self._initialized = True
+
+    def get_help(self, url):
+        """
+        """
+        self.initialize()
+        if url.scheme() == "help" and url.authority() == "search":
+            return self.search(qurl_query_items(url))
+        else:
+            return url
+
+    def description_by_id(self, desc_id):
+        reg = self._registry
+        return get_by_id(reg, desc_id)
+
+    def search(self, query):
+        self.initialize()
+
+        if isinstance(query, QUrl):
+            query = qurl_query_items(query)
+
+        query = dict(query)
+        desc_id = query["id"]
+        desc = self.description_by_id(desc_id)
+
+        provider = None
+        if desc.project_name:
+            provider = self._providers.get(desc.project_name)
+
+        # TODO: Ensure initialization of the provider
+        if provider:
+            return provider.search(desc)
+        else:
+            raise KeyError(desc_id)
+
+
+def get_by_id(registry, descriptor_id):
+    for desc in registry.widgets():
+        if desc.id == descriptor_id:
+            return desc
+
+    raise KeyError(descriptor_id)
+
+
+def qurl_query_items(url):
+    items = []
+    for key, value in url.queryItems():
+        items.append((unicode(key), unicode(value)))
+    return items
+
+
+def get_help_provider_for_description(desc):
+    if desc.project_name:
+        dist = pkg_resources.get_distribution(desc.project_name)
+        return get_help_provider_for_distribution(dist)
+
+
+def is_develop_egg(dist):
+    """
+    Is the distribution installed in development mode (setup.py develop)
+    """
+    meta_provider = dist._provider
+    egg_info_dir = os.path.dirname(meta_provider.egg_info)
+    egg_name = pkg_resources.to_filename(dist.project_name)
+    return meta_provider.egg_info.endswith(egg_name + ".egg-info") \
+           and os.path.exists(os.path.join(egg_info_dir, "setup.py"))
+
+
+def left_trim_lines(lines):
+    """
+    Remove all unnecessary leading space from lines.
+    """
+    lines_striped = zip(lines[1:], map(string.lstrip, lines[1:]))
+    lines_striped = filter(itemgetter(1), lines_striped)
+    indent = min([len(line) - len(striped) \
+                  for line, striped in lines_striped] + [sys.maxint])
+
+    if indent < sys.maxint:
+        return [line[indent:] for line in lines]
+    else:
+        return list(lines)
+
+
+def trim_trailing_lines(lines):
+    """
+    Trim trailing blank lines.
+    """
+    lines = list(lines)
+    while lines and not lines[-1]:
+        lines.pop(-1)
+    return lines
+
+
+def trim_leading_lines(lines):
+    """
+    Trim leading blank lines.
+    """
+    lines = list(lines)
+    while lines and not lines[0]:
+        lines.pop(0)
+    return lines
+
+
+def trim(string):
+    """
+    Trim a string in PEP-256 compatible way
+    """
+    lines = string.expandtabs().splitlines()
+
+    lines = map(str.lstrip, lines[:1]) + left_trim_lines(lines[1:])
+
+    return  "\n".join(trim_leading_lines(trim_trailing_lines(lines)))
+
+
+def parse_pkg_info(contents):
+    lines = contents.expandtabs().splitlines()
+    parsed = {}
+    current_block = None
+    for line in lines:
+        if line.startswith(" "):
+            parsed[current_block].append(line)
+        elif line.strip():
+            current_block, block_contents = line.split(": ", 1)
+            if current_block == "Classifier":
+                if current_block not in parsed:
+                    parsed[current_block] = [trim(block_contents)]
+                else:
+                    parsed[current_block].append(trim(block_contents))
+            else:
+                parsed[current_block] = [block_contents]
+
+    for key, val in parsed.items():
+        if key != "Classifier":
+            parsed[key] = trim("\n".join(val))
+
+    return parsed
+
+
+def get_pkg_info_entry(dist, name):
+    """
+    Get the contents of the named entry from the distributions PKG-INFO file
+    """
+    pkg_info = parse_pkg_info(dist.get_metadata("PKG-INFO"))
+    return pkg_info[name]
+
+
+def get_dist_url(dist):
+    """
+    Return the 'url' of the distribution (as passed to setup function)
+    """
+    return get_pkg_info_entry(dist, "Home-page")
+
+
+def create_intersphinx_provider(entry_point):
+    locations = entry_point.load()
+    dist = entry_point.dist
+
+    replacements = {"PROJECT_NAME": dist.project_name,
+                    "PROJECT_NAME_LOWER": dist.project_name.lower(),
+                    "PROJECT_VERSION": dist.version}
+    try:
+        replacements["URL"] = get_dist_url(dist)
+    except KeyError:
+        pass
+
+    formatter = string.Formatter()
+
+    for target, inventory in locations:
+        # Extract all format fields
+        format_iter = formatter.parse(target)
+        if inventory:
+            format_iter = itertools.chain(format_iter,
+                                          formatter.parse(inventory))
+        fields = map(itemgetter(1), format_iter)
+        fields = filter(None, set(fields))
+
+        if "DEVELOP_ROOT" in fields and is_develop_egg(dist):
+            target = formatter.format(target, DEVELOP_ROOT=dist.location)
+
+            if os.path.exists(target) and \
+                    os.path.exists(os.path.join(target, "objects.inv")):
+                return IntersphinxHelpProvider(target=target)
+            else:
+                continue
+        elif fields:
+            try:
+                target = formatter.format(target, **replacements)
+                if inventory:
+                    inventory = formatter.format(inventory, **replacements)
+            except KeyError:
+                log.exception("Error while formating intersphinx url.")
+                continue
+
+            return IntersphinxHelpProvider(target=target, inventory=inventory)
+        else:
+            return IntersphinxHelpProvider(target=target, inventory=inventory)
+
+    return None
+
+
+_providers = {"intersphinx": create_intersphinx_provider}
+
+
+def get_help_provider_for_distribution(dist):
+    entry_points = dist.get_entry_map().get("orange.canvas.help", {})
+    provider = None
+    for name, entry_point in entry_points.items():
+        create = _providers.get(name, None)
+        if create:
+            try:
+                provider = create(entry_point)
+            except pkg_resources.DistributionNotFound as err:
+                log.warning("Unsatisfied dependencies (%r)", err)
+                continue
+            except Exception:
+                log.exception("Exception")
+            if provider:
+                log.info("Created %s provider for %s",
+                         type(provider), dist)
+                break
+
+    return provider

Orange/OrangeCanvas/help/provider.py

+"""
+
+"""
+import os
+import logging
+
+from StringIO import StringIO
+from urlparse import urljoin
+
+from PyQt4.QtCore import QObject, QUrl
+
+from PyQt4.QtNetwork import (
+    QNetworkAccessManager, QNetworkDiskCache, QNetworkRequest, QNetworkReply
+)
+
+
+from .intersphinx import read_inventory_v1, read_inventory_v2
+
+from .. import config
+
+log = logging.getLogger(__name__)
+
+
+class HelpProvider(QObject):
+    def __init__(self, parent=None):
+        QObject.__init__(self, parent)
+
+    def search(self, description):
+        raise NotImplementedError
+
+
+class IntersphinxHelpProvider(HelpProvider):
+    def __init__(self, parent=None, target=None, inventory=None):
+        HelpProvider.__init__(self, parent)
+        self.target = target
+
+        if inventory is None:
+            if is_url_local(self.target):
+                inventory = os.path.join(self.target, "objects.inv")
+            else:
+                inventory = urljoin(self.target, "objects.inv")
+
+        self.inventory = inventory
+
+        self.islocal = bool(QUrl(inventory).toLocalFile())
+
+        self._fetch_inventory()
+
+    def search(self, description):
+        if description.help_ref:
+            ref = description.help_ref
+        else:
+            ref = description.name
+
+        if not hasattr(self, "items"):
+            self._reply.waitForReadyRead(2000)
+
+        labels = self.items.get("std:label", {})
+        entry = labels.get(ref.lower(), None)
+        if entry is not None:
+            _, _, url, _ = entry
+            return url
+        else:
+            raise KeyError(ref)
+
+    def _fetch_inventory(self):
+        cache_dir = config.cache_dir()
+        cache_dir = os.path.join(cache_dir, "help", "intersphinx")
+
+        try:
+            os.makedirs(cache_dir)
+        except OSError:
+            pass
+
+        url = QUrl(self.inventory)
+
+        if not self.islocal:
+            # fetch and cache the inventory file
+            manager = QNetworkAccessManager(self)
+            cache = QNetworkDiskCache()
+            cache.setCacheDirectory(cache_dir)
+            manager.setCache(cache)
+            req = QNetworkRequest(url)
+
+            self._reply = manager.get(req)
+            manager.finished.connect(self._on_finished)
+        else:
+            self._load_inventory(open(unicode(url.toLocalFile()), "rb"))
+
+    def _on_finished(self, reply):
+        del self._reply
+        if reply.error() != QNetworkReply.NoError:
+            log.error("An error occurred while fetching "
+                      "intersphinx inventory {0!r}".format(self.inventory))
+
+            self._error = reply.error(), reply.errorString()
+
+        else:
+            contents = reply.readAll()
+            self._load_inventory(StringIO(contents))
+
+    def _load_inventory(self, stream):
+        version = stream.readline().rstrip()
+        if self.islocal:
+            join = os.path.join
+        else:
+            join = urljoin
+
+        if version == "# Sphinx inventory version 1":
+            inventory = read_inventory_v1(stream, self.target, join)
+        elif version == "# Sphinx inventory version 2":
+            inventory = read_inventory_v2(stream, self.target, join)
+        else:
+            log.error("Invalid/unknown intersphinx inventory format.")
+            self._error = (ValueError,
+                           "{0} does not seem to be an intersphinx "
+                           "inventory file".format(self.target))
+
+        self.items = inventory
+
+
+def qurl_query_items(url):
+    items = []
+    for key, value in url.queryItems():
+        items.append((unicode(key), unicode(value)))
+    return items
+
+
+def is_url_local(url):
+    return bool(QUrl(url).toLocalFile())

Orange/OrangeCanvas/registry/base.py

 log = logging.getLogger(__name__)
 
 # Registry hex version
-VERSION_HEX = 0x000101
+VERSION_HEX = 0x000102
 
 
 class WidgetRegistry(object):

Orange/OrangeCanvas/registry/description.py

         A list of output channels provided by the widget.
     help : str, optional
         URL or an Resource template of a detailed widget help page.
+    help_ref: str, optional
+        A text reference id that can be used to identify the help
+        page, for instance an intersphinx reference.
     author : str, optional
         Author name.
     author_email : str, optional
                  inputs=[], outputs=[],
                  author=None, author_email=None,
                  maintainer=None, maintainer_email=None,
-                 help=None, url=None, keywords=None,
+                 help=None, help_ref=None, url=None, keywords=None,
                  priority=sys.maxint,
                  icon=None, background=None,
                  replaces=None,
         self.inputs = inputs
         self.outputs = outputs
         self.help = help
+        self.help_ref = help_ref
         self.author = author
         self.author_email = author_email
         self.maintainer = maintainer
         maintainer = getattr(module, "MAINTAINER", None)
         maintainer_email = getattr(module, "MAINTAINER_EMAIL", None)
         help = getattr(module, "HELP", None)
+        help_ref = getattr(module, "HELP_REF", None)
         url = getattr(module, "URL", None)
 
         icon = getattr(module, "ICON", None)
             maintainer=maintainer,
             maintainer_email=maintainer_email,
             help=help,
+            help_ref=help_ref,
             url=url,
             keywords=keywords,
             priority=priority,
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.