source: orange/Orange/OrangeCanvas/registry/discovery.py @ 11285:c0fc66213bfd

Revision 11285:c0fc66213bfd, 18.2 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Changed discovery to run from arbitrary entry points iterator/group.

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