source: orange/Orange/OrangeCanvas/help/manager.py @ 11688:3abfd8a227d0

Revision 11688:3abfd8a227d0, 8.3 KB checked in by Ales Erjavec <ales.erjavec@…>, 7 months ago (diff)

Added support for reading distribution meta info from .dist-info directory.

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