source: orange/Orange/OrangeCanvas/registry/discovery.py @ 11282:099b5661fe8e

Revision 11282:099b5661fe8e, 18.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Downgraded log severity in widget discovery phase.

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.info("Could not import %r.", name, exc_info=True)
301                        continue
302                    except Exception:
303                        log.warning("Error while importing %r.", name,
304                                    exc_info=True)
305                        continue
306
307                    try:
308                        desc = self.widget_description(
309                                 module,
310                                 category_name=category_name,
311                                 distribution=distribution
312                                 )
313                    except WidgetSpecificationError:
314                        self.cache_log_error(
315                                 source_path, WidgetSpecificationError,
316                                 distribution
317                                 )
318
319                        continue
320                    except Exception:
321                        log.warning("Problem parsing %r", name, exc_info=True)
322                        continue
323                yield desc
324                self.cache_insert(source_path, os.stat(source_path).st_mtime,
325                                  desc, distribution)
326
327    def widget_description(self, module, widget_name=None,
328                           category_name=None, distribution=None):
329        """Return a widget description from a module.
330        """
331        if isinstance(module, basestring):
332            module = __import__(module, fromlist=[""])
333
334        desc = None
335        try:
336            desc = WidgetDescription.from_module(module)
337        except WidgetSpecificationError:
338            exc_info = sys.exc_info()
339
340        if desc is None:
341            # Is it an old style widget file.
342            try:
343                desc = WidgetDescription.from_file(
344                    module.__file__, import_name=module.__name__
345                    )
346            except WidgetSpecificationError:
347                pass
348            except Exception:
349                pass
350
351        if desc is None:
352            # Raise the original exception.
353            raise exc_info
354
355        if widget_name is not None:
356            desc.name = widget_name
357
358        if category_name is not None:
359            desc.category = category_name
360
361        if distribution is not None:
362            desc.project_name = distribution.project_name
363
364        return desc
365
366    def cache_insert(self, module, mtime, description, distribution=None,
367                     error=None):
368        """Insert the description into the cache.
369        """
370        if isinstance(module, types.ModuleType):
371            mod_path = module.__file__
372            mod_name = module.__name__
373        else:
374            mod_path = module
375            mod_name = None
376        mod_path = fix_pyext(mod_path)
377
378        project_name = project_version = None
379
380        if distribution is not None:
381            project_name = distribution.project_name
382            project_version = distribution.version
383
384        exc_type = exc_val = None
385
386        if error is not None:
387            if isinstance(error, type):
388                exc_type = error
389                exc_val = None
390            elif isinstance(error, Exception):
391                exc_type = type(error)
392                exc_val = repr(error.args)
393
394        self.cached_descriptions[mod_path] = \
395                _CacheEntry(mod_path, mod_name, mtime, project_name,
396                            project_version, exc_type, exc_val,
397                            description)
398
399    def cache_get(self, mod_path, distribution=None):
400        """Get the cache entry for mod_path.
401        """
402        if isinstance(mod_path, types.ModuleType):
403            mod_path = mod_path.__file__
404        mod_path = fix_pyext(mod_path)
405        return self.cached_descriptions.get(mod_path)
406
407    def cache_has_valid_entry(self, mod_path, distribution=None):
408        """Does the cache have a valid entry for mod_path.
409        """
410        mod_path = fix_pyext(mod_path)
411
412        if not os.path.exists(mod_path):
413            return False
414
415        if mod_path in self.cached_descriptions:
416            entry = self.cache_get(mod_path)
417            mtime = os.stat(mod_path).st_mtime
418            if entry.mtime != mtime:
419                return False
420
421            if distribution is not None:
422                if entry.project_name != distribution.project_name or \
423                        entry.project_version != distribution.version:
424                    return False
425
426            if entry.exc_type == WidgetSpecificationError:
427                return False
428
429            # All checks pass
430            return True
431
432        return False
433
434    def cache_can_ignore(self, mod_path, distribution=None):
435        """Can the mod_path be ignored (i.e. it was determined that it
436        could not contain a valid widget description, for instance the
437        module does not hava a valid description and was not changed from
438        the last run).
439
440        """
441        mod_path = fix_pyext(mod_path)
442        if not os.path.exists(mod_path):
443            # Possible orphaned .pyc file
444            return True
445
446        mtime = os.stat(mod_path).st_mtime
447        if mod_path in self.cached_descriptions:
448            entry = self.cached_descriptions[mod_path]
449            return entry.mtime == mtime and \
450                    entry.exc_type == WidgetSpecificationError
451        else:
452            return False
453
454    def cache_log_error(self, mod_path, error, distribution=None):
455        """Cache that the `error` occurred while processing mod_path.
456        """
457        mod_path = fix_pyext(mod_path)
458        if not os.path.exists(mod_path):
459            # Possible orphaned .pyc file
460            return
461        mtime = os.stat(mod_path).st_mtime
462
463        self.cache_insert(mod_path, mtime, None, distribution, error)
464
465
466def fix_pyext(mod_path):
467    """Fix a module filename path extension to always end with the
468    modules source file (i.e. strip compiled/optimzed .pyc,p.pyo
469    extension and replace it with .py).
470
471    """
472    if mod_path[-4:] in [".pyo", "pyc"]:
473        mod_path = mod_path[:-1]
474    return mod_path
475
476
477def widget_descriptions_from_package(package):
478    package = asmodule(package)
479
480    desciptions = []
481    for _, name, ispkg in pkgutil.iter_modules(
482            package.__path__, package.__name__ + "."):
483        if ispkg:
484            continue
485        try:
486            module = asmodule(name)
487        except Exception:
488            log.error("Error importing %r.", name, exc_info=True)
489            continue
490
491        desc = None
492        try:
493            desc = WidgetDescription.from_module(module)
494        except Exception:
495            pass
496        if not desc:
497            try:
498                desc = WidgetDescription.from_file(
499                    module.__file__, import_name=name
500                    )
501            except Exception:
502                pass
503
504        if not desc:
505            log.info("Error in %r", name, exc_info=True)
506        else:
507            desciptions.append(desc)
508    return desciptions
509
510
511def module_name_split(name):
512    """Split the module name into pacakge name and module
513    name inside the pacakge.
514
515    """
516    if "." in name:
517        package_name, module = name.rsplit(".", 1)
518    else:
519        package_name, module = "", module
520    return package_name, module
521
522
523def module_modified_time(module):
524    """Return the `module`s source filename and modified time as a tuple
525    (source_filename, modified_time). In case the module is from a zipped
526    package the modified time is that of of the archive.
527
528    """
529    module = asmodule(module)
530    name = module.__name__
531    module_filename = module.__file__
532
533#    if os.path.splitext(module_filename)[-1] in [".pyc", ".pyo"]:
534#        module_filename = module_filename[:-1]
535
536    provider = pkg_resources.get_provider(name)
537    if provider.loader:
538        m_time = os.stat(provider.loader.archive)[stat.ST_MTIME]
539    else:
540        basename = os.path.basename(module_filename)
541        path = pkg_resources.resource_filename(name, basename)
542        m_time = os.stat(path)[stat.ST_MTIME]
543    return (module_filename, m_time)
544
545
546def asmodule(module):
547    """Return the module references by `module` name. If `module` is
548    already an imported module instance, return it as is.
549
550    """
551    if isinstance(module, types.ModuleType):
552        return module
553    elif isinstance(module, basestring):
554        return __import__(module, fromlist=[""])
555    else:
556        raise TypeError(type(module))
557
558
559def run_discovery(cached=False):
560    """Run the default widget discovery and return the WidgetRegisty
561    instance.
562
563    """
564    from . import cache, WidgetRegistry
565    reg_cache = {}
566    if cached:
567        reg_cache = cache.registry_cache()
568
569    registry = WidgetRegistry()
570    discovery = WidgetDiscovery(registry, cached_descriptions=reg_cache)
571    discovery.run()
572    if cached:
573        cache.save_registry_cache(reg_cache)
574    return registry
Note: See TracBrowser for help on using the repository browser.