source: orange/Orange/OrangeCanvas/registry/discovery.py @ 11098:b743937fe90a

Revision 11098:b743937fe90a, 18.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 19 months ago (diff)

Added new widget/category description classes and new widget discovery.

This includes a Qt item model interface for the registry.

Line 
1"""
2Widget discovery.
3
4"""
5
6import os
7import sys
8import stat
9import glob
10import logging
11import itertools
12import types
13import pkgutil
14from collections import namedtuple
15import pkg_resources
16
17from .description import (
18    WidgetDescription, CategoryDescription,
19    WidgetSpecificationError, CategorySpecificationError
20)
21
22from . import VERSION_HEX
23
24
25log = logging.getLogger(__name__)
26
27
28_CacheEntry = \
29    namedtuple(
30        "_CacheEntry",
31        ["mod_path",         # Module path (filename)
32         "name",             # Module qualified import name
33         "mtime",            # Modified time
34         "project_name",     # distribution name (if available)
35         "project_version",  # distribution version (if available)
36         "exc_type",         # exception type
37         "exc_val",          # exception value (string)
38         "description"       # WidgetDescription instance
39         ]
40    )
41
42
43def default_category_for_module(module):
44    """Return a default constructed :class:`CategoryDescription`
45    for `module`.
46
47    """
48    if isinstance(module, basestring):
49        module = __import__(module, fromlist=[""])
50    name = module.__name__.rsplit(".", 1)[-1]
51    qualified_name = module.__name__
52    return CategoryDescription(name=name, qualified_name=qualified_name)
53
54
55WIDGETS_ENTRY = "orange.widgets"
56
57
58# This could also be achieved by declaring the entry point in
59# Orange's setup.py
60def default_entry_point():
61    """Return a default orange.widgets entry point for loading
62    default Orange Widgets.
63
64    """
65    dist = pkg_resources.get_distribution("Orange")
66    ep = pkg_resources.EntryPoint("Orange Widgets", "Orange.OrangeWidgets",
67                                  dist=dist)
68    return ep
69
70
71def widgets_entry_points():
72    """Return an EntryPoint iterator for all 'orange.widget' entry
73    points including the default Orange Widgets.
74
75    """
76    ep_iter = pkg_resources.iter_entry_points(WIDGETS_ENTRY)
77    chain = [[default_entry_point()],
78             ep_iter
79             ]
80    return itertools.chain(*chain)
81
82
83class WidgetDiscovery(object):
84    """Base widget discovery runner.
85    """
86
87    def __init__(self, registry=None, cached_descriptions=None):
88        self.registry = registry
89        self.cached_descriptions = cached_descriptions or {}
90        version = (VERSION_HEX, )
91        if self.cached_descriptions.get("!VERSION") != version:
92            self.cached_descriptions.clear()
93            self.cached_descriptions["!VERSION"] = version
94
95    def run(self):
96        """Run the widget discovery process.
97        """
98        for entry_point in widgets_entry_points():
99            try:
100                point = entry_point.load()
101            except pkg_resources.DistributionNotFound:
102                log.error("Could not load %r (unsatisfied dependencies).",
103                          entry_point.name, exc_info=True)
104                continue
105            except ImportError:
106                log.error("An ImportError occurred while loading an "
107                          "entry point", exc_info=True)
108                continue
109            except Exception:
110                log.error("An exception occurred while loading an "
111                          "entry point", exc_info=True)
112                continue
113
114            try:
115                if isinstance(point, types.ModuleType):
116                    if point.__path__:
117                        # Entry point is a package (a widget category)
118                        self.process_category_package(
119                            point,
120                            name=entry_point.name,
121                            distribution=entry_point.dist
122                        )
123                    else:
124                        # Entry point is a module (a single widget)
125                        self.process_widget_module(
126                            point,
127                            name=entry_point.name,
128                            distribution=entry_point.dist
129                        )
130                elif isinstance(point, (types.FunctionType, types.MethodType)):
131                    # Entry point is a callable loader function
132                    self.process_loader(point)
133                elif isinstance(point, (list, tuple)):
134                    # An iterator yielding Category/WidgetDescriptor instances.
135                    self.process_iter(point)
136                else:
137                    log.error("Cannot handle entry point %r", point)
138            except Exception:
139                log.error("An exception occurred while processing %r.",
140                          entry_point, exc_info=True)
141
142    def process_directory(self, directory):
143        """Process and locate any widgets in directory
144        (old style widget discovery).
145
146        """
147        for filename in glob.glob(os.path.join(directory, "*.py")):
148            self.process_file(filename)
149
150    def process_file(self, filename):
151        """Process .py file containing the widget code
152        (old stye widget discovery).
153
154        """
155        filename = fix_pyext(filename)
156        # TODO: zipped modules
157        if self.cache_has_valid_entry(filename):
158            desc = self.cache_get(filename).description
159            return
160
161        if self.cache_can_ignore(filename):
162            log.info("Ignoring %r.", filename)
163            return
164
165        try:
166            desc = WidgetDescription.from_file(filename)
167        except WidgetSpecificationError:
168            self.cache_insert(filename, 0, None, None,
169                              WidgetSpecificationError)
170            return
171        except Exception:
172            log.info("Error processing a file.", exc_info=True)
173            return
174
175        self.handle_widget(desc)
176
177    def process_widget_module(self, module, name=None, distribution=None):
178        """Process a widget module.
179        """
180        try:
181            desc = self.widget_description(module, widget_name=name,
182                                           distribution=distribution)
183        except (WidgetSpecificationError, Exception), ex:
184            log.info("Invalid widget specification.", exc_info=True)
185            return
186
187        self.handle_widget(desc)
188
189    def process_category_package(self, category, name=None, distribution=None):
190        """Process a category package.
191        """
192        cat_desc = None
193        category = asmodule(category)
194
195        if hasattr(category, "widget_discovery"):
196            widget_discovery = getattr(category, "widget_discovery")
197            self.process_loader(widget_discovery)
198            return  # The widget_discovery function handles all
199        elif hasattr(category, "category_description"):
200            category_description = getattr(category, "category_description")
201            try:
202                cat_desc = category_description()
203            except Exception:
204                log.error("Error calling 'category_description' in %r.",
205                          category, exc_info=True)
206                cat_desc = default_category_for_module(category)
207        else:
208            try:
209                cat_desc = CategoryDescription.from_package(category)
210            except (CategorySpecificationError, Exception), ex:
211                log.info("Package %r does not describe a category.", category,
212                         exc_info=True)
213                cat_desc = default_category_for_module(category)
214
215        if name is not None:
216            cat_desc.name = name
217
218        if distribution is not None:
219            cat_desc.project_name = distribution.project_name
220
221        self.handle_category(cat_desc)
222
223        desc_iter = self.iter_widget_descriptions(
224                        category,
225                        category_name=cat_desc.name,
226                        distribution=distribution
227                        )
228
229        for desc in desc_iter:
230            self.handle_widget(desc)
231
232    def process_loader(self, callable):
233        """Process a callable loader function.
234        """
235        try:
236            callable(self)
237        except Exception:
238            log.error("Error calling %r", callable, exc_info=True)
239
240    def process_iter(self, iter):
241        """
242        """
243        for desc in iter:
244            if isinstance(desc, CategoryDescription):
245                self.handle_category(desc)
246            elif isinstance(desc, WidgetDescription):
247                self.handle_widget(desc)
248            else:
249                log.error("Category or Widget Description instance expected."
250                          "Got %r.", desc)
251
252    def handle_widget(self, desc):
253        """Handle a found widget description.
254        Base implementation adds it to the registry supplied in the
255        constructor.
256
257        """
258        if self.registry:
259            self.registry.register_widget(desc)
260
261    def handle_category(self, desc):
262        """Handle a found category description.
263        Base implementation adds it to the registry supplied in the
264        constructor.
265
266        """
267        if self.registry:
268            self.registry.register_category(desc)
269
270    def iter_widget_descriptions(self, package, category_name=None,
271                                 distribution=None):
272        """Return an iterator over widget descriptions
273        accessible from `package`.
274
275        """
276        package = asmodule(package)
277
278        for path in package.__path__:
279            for _, mod_name, ispkg in pkgutil.iter_modules([path]):
280                if ispkg:
281                    continue
282                name = package.__name__ + "." + mod_name
283                source_path = os.path.join(path, mod_name + ".py")
284                desc = None
285
286                # Check if the path can be ignored.
287                if self.cache_can_ignore(source_path, distribution):
288                    log.info("Ignoring %r.", source_path)
289                    continue
290
291                # Check if a source file for the module is available
292                # and is already cached.
293                if self.cache_has_valid_entry(source_path, distribution):
294                    desc = self.cache_get(source_path).description
295
296                if desc is None:
297                    try:
298                        module = asmodule(name)
299                    except ImportError:
300                        log.warning("Could not import %r.", name,
301                                    exc_info=True)
302                        continue
303                    except Exception:
304                        log.error("Error while importing %r.", name,
305                                  exc_info=True)
306                        continue
307
308                    try:
309                        desc = self.widget_description(
310                                 module,
311                                 category_name=category_name,
312                                 distribution=distribution
313                                 )
314                    except WidgetSpecificationError:
315                        self.cache_log_error(
316                                 source_path, WidgetSpecificationError,
317                                 distribution
318                                 )
319
320                        continue
321                    except Exception:
322                        log.warning("Problem parsing %r", name, exc_info=True)
323                        continue
324                yield desc
325                self.cache_insert(source_path, os.stat(source_path).st_mtime,
326                                  desc, distribution)
327
328    def widget_description(self, module, widget_name=None,
329                           category_name=None, distribution=None):
330        """Return a widget description from a module.
331        """
332        if isinstance(module, basestring):
333            module = __import__(module, fromlist=[""])
334
335        desc = None
336        try:
337            desc = WidgetDescription.from_module(module)
338        except WidgetSpecificationError:
339            exc_info = sys.exc_info()
340
341        if desc is None:
342            # Is it an old style widget file.
343            try:
344                desc = WidgetDescription.from_file(
345                    module.__file__, import_name=module.__name__
346                    )
347            except WidgetSpecificationError:
348                pass
349            except Exception:
350                pass
351
352        if desc is None:
353            # Raise the original exception.
354            raise exc_info
355
356        if widget_name is not None:
357            desc.name = widget_name
358
359        if category_name is not None:
360            desc.category = category_name
361
362        if distribution is not None:
363            desc.project_name = distribution.project_name
364
365        return desc
366
367    def cache_insert(self, module, mtime, description, distribution=None,
368                     error=None):
369        """Insert the description into the cache.
370        """
371        if isinstance(module, types.ModuleType):
372            mod_path = module.__file__
373            mod_name = module.__name__
374        else:
375            mod_path = module
376            mod_name = None
377        mod_path = fix_pyext(mod_path)
378
379        project_name = project_version = None
380
381        if distribution is not None:
382            project_name = distribution.project_name
383            project_version = distribution.version
384
385        exc_type = exc_val = None
386
387        if error is not None:
388            if isinstance(error, type):
389                exc_type = error
390                exc_val = None
391            elif isinstance(error, Exception):
392                exc_type = type(error)
393                exc_val = repr(error.args)
394
395        self.cached_descriptions[mod_path] = \
396                _CacheEntry(mod_path, mod_name, mtime, project_name,
397                            project_version, exc_type, exc_val,
398                            description)
399
400    def cache_get(self, mod_path, distribution=None):
401        """Get the cache entry for mod_path.
402        """
403        if isinstance(mod_path, types.ModuleType):
404            mod_path = mod_path.__file__
405        mod_path = fix_pyext(mod_path)
406        return self.cached_descriptions.get(mod_path)
407
408    def cache_has_valid_entry(self, mod_path, distribution=None):
409        """Does the cache have a valid entry for mod_path.
410        """
411        mod_path = fix_pyext(mod_path)
412
413        if not os.path.exists(mod_path):
414            return False
415
416        if mod_path in self.cached_descriptions:
417            entry = self.cache_get(mod_path)
418            mtime = os.stat(mod_path).st_mtime
419            if entry.mtime != mtime:
420                return False
421
422            if distribution is not None:
423                if entry.project_name != distribution.project_name or \
424                        entry.project_version != distribution.version:
425                    return False
426
427            if entry.exc_type == WidgetSpecificationError:
428                return False
429
430            # All checks pass
431            return True
432
433        return False
434
435    def cache_can_ignore(self, mod_path, distribution=None):
436        """Can the mod_path be ignored (i.e. it was determined that it
437        could not contain a valid widget description, for instance the
438        module does not hava a valid description and was not changed from
439        the last run).
440
441        """
442        mod_path = fix_pyext(mod_path)
443        if not os.path.exists(mod_path):
444            # Possible orphaned .pyc file
445            return True
446
447        mtime = os.stat(mod_path).st_mtime
448        if mod_path in self.cached_descriptions:
449            entry = self.cached_descriptions[mod_path]
450            return entry.mtime == mtime and \
451                    entry.exc_type == WidgetSpecificationError
452        else:
453            return False
454
455    def cache_log_error(self, mod_path, error, distribution=None):
456        """Cache that the `error` occurred while processing mod_path.
457        """
458        mod_path = fix_pyext(mod_path)
459        if not os.path.exists(mod_path):
460            # Possible orphaned .pyc file
461            return
462        mtime = os.stat(mod_path).st_mtime
463
464        self.cache_insert(mod_path, mtime, None, distribution, error)
465
466
467def fix_pyext(mod_path):
468    """Fix a module filename path extension to always end with the
469    modules source file (i.e. strip compiled/optimzed .pyc,p.pyo
470    extension and replace it with .py).
471
472    """
473    if mod_path[-4:] in [".pyo", "pyc"]:
474        mod_path = mod_path[:-1]
475    return mod_path
476
477
478def widget_descriptions_from_package(package):
479    package = asmodule(package)
480
481    desciptions = []
482    for _, name, ispkg in pkgutil.iter_modules(
483            package.__path__, package.__name__ + "."):
484        if ispkg:
485            continue
486        try:
487            module = asmodule(name)
488        except Exception:
489            log.error("Error importing %r.", name, exc_info=True)
490            continue
491
492        desc = None
493        try:
494            desc = WidgetDescription.from_module(module)
495        except Exception:
496            pass
497        if not desc:
498            try:
499                desc = WidgetDescription.from_file(
500                    module.__file__, import_name=name
501                    )
502            except Exception:
503                pass
504
505        if not desc:
506            log.info("Error in %r", name, exc_info=True)
507        else:
508            desciptions.append(desc)
509    return desciptions
510
511
512def module_name_split(name):
513    """Split the module name into pacakge name and module
514    name inside the pacakge.
515
516    """
517    if "." in name:
518        package_name, module = name.rsplit(".", 1)
519    else:
520        package_name, module = "", module
521    return package_name, module
522
523
524def module_modified_time(module):
525    """Return the `module`s source filename and modified time as a tuple
526    (source_filename, modified_time). In case the module is from a zipped
527    package the modified time is that of of the archive.
528
529    """
530    module = asmodule(module)
531    name = module.__name__
532    module_filename = module.__file__
533
534#    if os.path.splitext(module_filename)[-1] in [".pyc", ".pyo"]:
535#        module_filename = module_filename[:-1]
536
537    provider = pkg_resources.get_provider(name)
538    if provider.loader:
539        m_time = os.stat(provider.loader.archive)[stat.ST_MTIME]
540    else:
541        basename = os.path.basename(module_filename)
542        path = pkg_resources.resource_filename(name, basename)
543        m_time = os.stat(path)[stat.ST_MTIME]
544    return (module_filename, m_time)
545
546
547def asmodule(module):
548    """Return the module references by `module` name. If `module` is
549    already an imported module instance, return it as is.
550
551    """
552    if isinstance(module, types.ModuleType):
553        return module
554    elif isinstance(module, basestring):
555        return __import__(module, fromlist=[""])
556    else:
557        raise TypeError(type(module))
558
559
560def run_discovery(cached=False):
561    """Run the default widget discovery and return the WidgetRegisty
562    instance.
563
564    """
565    from . import cache, WidgetRegistry
566    reg_cache = {}
567    if cached:
568        reg_cache = cache.registry_cache()
569
570    registry = WidgetRegistry()
571    discovery = WidgetDiscovery(registry, cached_descriptions=reg_cache)
572    discovery.run()
573    if cached:
574        cache.save_registry_cache(reg_cache)
575    return registry
Note: See TracBrowser for help on using the repository browser.