source: orange/Orange/OrangeCanvas/help/manager.py @ 11263:9f8fc6d26e58

Revision 11263:9f8fc6d26e58, 8.0 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Added canvas help search system.

Line 
1"""
2
3"""
4import sys
5import os
6import string
7import itertools
8import logging
9
10from operator import itemgetter
11
12import pkg_resources
13
14from .provider import IntersphinxHelpProvider
15
16from PyQt4.QtCore import QObject, QUrl
17
18log = logging.getLogger(__name__)
19
20
21class HelpManager(QObject):
22    def __init__(self, parent=None):
23        QObject.__init__(self, parent)
24        self._registry = None
25        self._initialized = False
26        self._providers = {}
27
28    def set_registry(self, registry):
29        """
30        Set the widget registry for which the manager should
31        provide help.
32
33        """
34        if self._registry is not registry:
35            self._registry = registry
36            self._initialized = False
37            self.initialize()
38
39    def registry(self):
40        """
41        Return the previously set with set_registry.
42        """
43        return self._registry
44
45    def initialize(self):
46        if self._initialized:
47            return
48
49        reg = self._registry
50        all_projects = set(desc.project_name for desc in reg.widgets())
51
52        providers = []
53        for project in set(all_projects) - set(self._providers.keys()):
54            provider = None
55            try:
56                dist = pkg_resources.get_distribution(project)
57                provider = get_help_provider_for_distribution(dist)
58            except Exception:
59                log.exception("Error while initializing help "
60                              "provider for %r", desc.project_name)
61
62            if provider:
63                providers.append((project, provider))
64                provider.setParent(self)
65
66        self._providers.update(dict(providers))
67        self._initialized = True
68
69    def get_help(self, url):
70        """
71        """
72        self.initialize()
73        if url.scheme() == "help" and url.authority() == "search":
74            return self.search(qurl_query_items(url))
75        else:
76            return url
77
78    def description_by_id(self, desc_id):
79        reg = self._registry
80        return get_by_id(reg, desc_id)
81
82    def search(self, query):
83        self.initialize()
84
85        if isinstance(query, QUrl):
86            query = qurl_query_items(query)
87
88        query = dict(query)
89        desc_id = query["id"]
90        desc = self.description_by_id(desc_id)
91
92        provider = None
93        if desc.project_name:
94            provider = self._providers.get(desc.project_name)
95
96        # TODO: Ensure initialization of the provider
97        if provider:
98            return provider.search(desc)
99        else:
100            raise KeyError(desc_id)
101
102
103def get_by_id(registry, descriptor_id):
104    for desc in registry.widgets():
105        if desc.id == descriptor_id:
106            return desc
107
108    raise KeyError(descriptor_id)
109
110
111def qurl_query_items(url):
112    items = []
113    for key, value in url.queryItems():
114        items.append((unicode(key), unicode(value)))
115    return items
116
117
118def get_help_provider_for_description(desc):
119    if desc.project_name:
120        dist = pkg_resources.get_distribution(desc.project_name)
121        return get_help_provider_for_distribution(dist)
122
123
124def is_develop_egg(dist):
125    """
126    Is the distribution installed in development mode (setup.py develop)
127    """
128    meta_provider = dist._provider
129    egg_info_dir = os.path.dirname(meta_provider.egg_info)
130    egg_name = pkg_resources.to_filename(dist.project_name)
131    return meta_provider.egg_info.endswith(egg_name + ".egg-info") \
132           and os.path.exists(os.path.join(egg_info_dir, "setup.py"))
133
134
135def left_trim_lines(lines):
136    """
137    Remove all unnecessary leading space from lines.
138    """
139    lines_striped = zip(lines[1:], map(string.lstrip, lines[1:]))
140    lines_striped = filter(itemgetter(1), lines_striped)
141    indent = min([len(line) - len(striped) \
142                  for line, striped in lines_striped] + [sys.maxint])
143
144    if indent < sys.maxint:
145        return [line[indent:] for line in lines]
146    else:
147        return list(lines)
148
149
150def trim_trailing_lines(lines):
151    """
152    Trim trailing blank lines.
153    """
154    lines = list(lines)
155    while lines and not lines[-1]:
156        lines.pop(-1)
157    return lines
158
159
160def trim_leading_lines(lines):
161    """
162    Trim leading blank lines.
163    """
164    lines = list(lines)
165    while lines and not lines[0]:
166        lines.pop(0)
167    return lines
168
169
170def trim(string):
171    """
172    Trim a string in PEP-256 compatible way
173    """
174    lines = string.expandtabs().splitlines()
175
176    lines = map(str.lstrip, lines[:1]) + left_trim_lines(lines[1:])
177
178    return  "\n".join(trim_leading_lines(trim_trailing_lines(lines)))
179
180
181def parse_pkg_info(contents):
182    lines = contents.expandtabs().splitlines()
183    parsed = {}
184    current_block = None
185    for line in lines:
186        if line.startswith(" "):
187            parsed[current_block].append(line)
188        elif line.strip():
189            current_block, block_contents = line.split(": ", 1)
190            if current_block == "Classifier":
191                if current_block not in parsed:
192                    parsed[current_block] = [trim(block_contents)]
193                else:
194                    parsed[current_block].append(trim(block_contents))
195            else:
196                parsed[current_block] = [block_contents]
197
198    for key, val in parsed.items():
199        if key != "Classifier":
200            parsed[key] = trim("\n".join(val))
201
202    return parsed
203
204
205def get_pkg_info_entry(dist, name):
206    """
207    Get the contents of the named entry from the distributions PKG-INFO file
208    """
209    pkg_info = parse_pkg_info(dist.get_metadata("PKG-INFO"))
210    return pkg_info[name]
211
212
213def get_dist_url(dist):
214    """
215    Return the 'url' of the distribution (as passed to setup function)
216    """
217    return get_pkg_info_entry(dist, "Home-page")
218
219
220def create_intersphinx_provider(entry_point):
221    locations = entry_point.load()
222    dist = entry_point.dist
223
224    replacements = {"PROJECT_NAME": dist.project_name,
225                    "PROJECT_NAME_LOWER": dist.project_name.lower(),
226                    "PROJECT_VERSION": dist.version}
227    try:
228        replacements["URL"] = get_dist_url(dist)
229    except KeyError:
230        pass
231
232    formatter = string.Formatter()
233
234    for target, inventory in locations:
235        # Extract all format fields
236        format_iter = formatter.parse(target)
237        if inventory:
238            format_iter = itertools.chain(format_iter,
239                                          formatter.parse(inventory))
240        fields = map(itemgetter(1), format_iter)
241        fields = filter(None, set(fields))
242
243        if "DEVELOP_ROOT" in fields and is_develop_egg(dist):
244            target = formatter.format(target, DEVELOP_ROOT=dist.location)
245
246            if os.path.exists(target) and \
247                    os.path.exists(os.path.join(target, "objects.inv")):
248                return IntersphinxHelpProvider(target=target)
249            else:
250                continue
251        elif fields:
252            try:
253                target = formatter.format(target, **replacements)
254                if inventory:
255                    inventory = formatter.format(inventory, **replacements)
256            except KeyError:
257                log.exception("Error while formating intersphinx url.")
258                continue
259
260            return IntersphinxHelpProvider(target=target, inventory=inventory)
261        else:
262            return IntersphinxHelpProvider(target=target, inventory=inventory)
263
264    return None
265
266
267_providers = {"intersphinx": create_intersphinx_provider}
268
269
270def get_help_provider_for_distribution(dist):
271    entry_points = dist.get_entry_map().get("orange.canvas.help", {})
272    provider = None
273    for name, entry_point in entry_points.items():
274        create = _providers.get(name, None)
275        if create:
276            try:
277                provider = create(entry_point)
278            except pkg_resources.DistributionNotFound as err:
279                log.warning("Unsatisfied dependencies (%r)", err)
280                continue
281            except Exception:
282                log.exception("Exception")
283            if provider:
284                log.info("Created %s provider for %s",
285                         type(provider), dist)
286                break
287
288    return provider
Note: See TracBrowser for help on using the repository browser.