root/releases/pkgcore/0.2.13/pkgcore/plugin.py @ marienz%2540gentoo.org-20070228103709-vaa6lao479k9saqv

Revision marienz%2540gentoo.org-20070228103709-vaa6lao479k9saqv, 6.9 KB (checked in by Marien Zwart <marienz@…>, 23 months ago)

Increase test coverage, move test_packages.quiet_logger to a place where it can be reused, fix a bug in dealing with a corrupt plugin cache.

Line 
1# Copyright: 2006 Marien Zwart <marienz@gentoo.org>
2# License: GPL2
3
4
5"""Plugin system, heavily inspired by twisted's plugin system."""
6
7# Implementation note: we have to be pretty careful about error
8# handling in here since some core functionality in pkgcore uses this
9# code. Since we can function without a cache we will generally be
10# noisy but keep working if something is wrong with the cache.
11#
12# Currently we explode if something is wrong with a plugin package
13# dir, but not if something prevents importing a module in it.
14# Rationale is the former should be a PYTHONPATH issue while the
15# latter an installed plugin issue. May have to change this if it
16# causes problems.
17
18import operator
19import os.path
20
21from pkgcore import plugins
22from pkgcore.util.osutils import join as pjoin
23from pkgcore.util import modules, demandload
24demandload.demandload(globals(), 'tempfile errno pkgcore.log:logger')
25
26
27# Global plugin cache. Mapping of package to package cache, which is a
28# mapping of plugin key to a list of module names.
29_cache = {}
30
31
32def initialize_cache(package):
33    """Determine available plugins in a package.
34
35    Writes cache files if they are stale and writing is possible.
36    """
37    # package plugin cache, see above.
38    package_cache = {}
39    seen_modnames = set()
40    for path in package.__path__:
41        # Check if the path actually exists first.
42        try:
43            modlist = os.listdir(path)
44        except OSError, e:
45            if e.errno != errno.ENOENT:
46                raise
47            continue
48        # Directory cache, mapping modulename to
49        # (mtime, set([keys]))
50        stored_cache = {}
51        stored_cache_name = pjoin(path, 'plugincache')
52        try:
53            cachefile = open(stored_cache_name)
54        except IOError:
55            # Something is wrong with the cache file. We just handle
56            # this as a missing/empty cache, which will force a
57            # rewrite. If whatever it is that is wrong prevents us
58            # from writing the new cache we log it there.
59            pass
60        else:
61            try:
62                # Remove this extra nesting once we require python 2.5
63                try:
64                    for line in cachefile:
65                        module, mtime, entries = line[:-1].split(':', 2)
66                        mtime = int(mtime)
67                        entries = set(entries.split(':'))
68                        stored_cache[module] = (mtime, entries)
69                except ValueError:
70                    # Corrupt cache, treat as empty.
71                    stored_cache = {}
72            finally:
73                cachefile.close()
74        cache_stale = False
75        # Hunt for modules.
76        actual_cache = {}
77        assumed_valid = set()
78        for modfullname in modlist:
79            modname, modext = os.path.splitext(modfullname)
80            if modext != '.py':
81                continue
82            if modname == '__init__':
83                continue
84            if modname in seen_modnames:
85                # This module is shadowed by a module earlier in
86                # sys.path. Skip it, assuming its cache is valid.
87                assumed_valid.add(modname)
88                continue
89            # It is an actual module. Check if its cache entry is valid.
90            mtime = int(os.path.getmtime(pjoin(path, modfullname)))
91            if mtime == stored_cache.get(modname, (0, ()))[0]:
92                # Cache is good, use it.
93                actual_cache[modname] = stored_cache[modname]
94            else:
95                # Cache entry is stale.
96                logger.debug(
97                    'stale because of %s: actual %s != stored %s',
98                    modname, mtime, stored_cache.get(modname, (0, ()))[0])
99                cache_stale = True
100                entries = []
101                qualname = '.'.join((package.__name__, modname))
102                try:
103                    module = modules.load_module(qualname)
104                except modules.FailedImport:
105                    # This is a serious problem, but if we blow up
106                    # here we cripple pkgcore entirely which may make
107                    # fixing the problem impossible. So be noisy but
108                    # try to continue.
109                    logger.exception('plugin import failed')
110                else:
111                    keys = set(getattr(module, 'pkgcore_plugins', ()))
112                    actual_cache[modname] = (mtime, keys)
113        # Cache is also stale if it sees entries that are no longer there.
114        for key in stored_cache:
115            if key not in actual_cache and key not in assumed_valid:
116                logger.debug('stale because %s is no longer there', key)
117                cache_stale = True
118                break
119        if cache_stale:
120            # Write a new cache.
121            try:
122                fd, name = tempfile.mkstemp(dir=path)
123            except OSError, e:
124                # We cannot write a new cache. We should log this
125                # since it will have a performance impact.
126
127                # Use error, not exception for this one: the traceback
128                # is not necessary and too alarming.
129                logger.error('Cannot write cache for %s: %s. '
130                             'Try running pplugincache.',
131                             stored_cache_name, e)
132            else:
133                cachefile = os.fdopen(fd, 'w')
134                try:
135                    for module, (mtime, entries) in actual_cache.iteritems():
136                        cachefile.write(
137                            '%s:%s:%s\n' % (module, mtime, ':'.join(entries)))
138                finally:
139                    cachefile.close()
140                os.chmod(name, 0644)
141                os.rename(name, stored_cache_name)
142        # Update the package_cache.
143        for module, (mtime, entries) in actual_cache.iteritems():
144            seen_modnames.add(module)
145            for key in entries:
146                package_cache.setdefault(key, []).append(module)
147    return package_cache
148
149
150def get_plugins(key, package=plugins):
151    """Return all enabled plugins matching "key".
152
153    Plugins with a C{disabled} attribute evaluating to C{True} are skipped.
154    """
155    cache = _cache.get(package)
156    if cache is None:
157        cache = _cache[package] = initialize_cache(package)
158    for modname in cache.get(key, ()):
159        module = modules.load_module('.'.join((package.__name__, modname)))
160        for obj in module.pkgcore_plugins.get(key, ()):
161            if not getattr(obj, 'disabled', False):
162                yield obj
163
164
165def get_plugin(key, package=plugins):
166    """Get a single plugin matching this key.
167
168    This assumes all plugins for this key have a priority attribute.
169    If any of them do not the AttributeError is not stopped.
170
171    @return: highest-priority plugin or None if no plugin available.
172    """
173    candidates = list(plugin for plugin in get_plugins(key, package))
174    if not candidates:
175        return None
176    candidates.sort(key=operator.attrgetter('priority'))
177    return candidates[-1]
Note: See TracBrowser for help on using the browser.