source: orange/Orange/OrangeCanvas/registry/discovery.py @ 11295:6f24900f54f0

Revision 11295:6f24900f54f0, 18.6 KB checked in by Ales Erjavec <ales.erjavec@…>, 15 months ago (diff)

Fixed an NameError in 'module_name_split' function.

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