root/releases/pkgcore-checks/0.3/pkgcore_checks/base.py @ ferringb%2540gmail.com-20070207174621-yb3eqrqc6jtmdgse

Revision ferringb%2540gmail.com-20070207174621-yb3eqrqc6jtmdgse, 17.0 KB (checked in by Brian Harring <ferringb@…>, 2 years ago)

kill off to_str, no longer used; use short_desc and long_desc properties instead

Line 
1# Copyright: 2006 Brian Harring <ferringb@gmail.com>
2# Copyright: 2006 Marien Zwart <marienz@gentoo.org>
3# License: GPL2
4
5
6"""Core classes and interfaces.
7
8This defines a couple of standard feed types and scopes. Currently
9feed types are strings and scopes are integers, but you should use the
10symbolic names wherever possible (everywhere except for adding a new
11feed type) since this might change in the future. Scopes are integers,
12but do not rely on that either.
13
14Feed types have to match exactly. Scopes are ordered: they define a
15minimally accepted scope, and for transforms the output scope is
16identical to the input scope.
17"""
18
19
20import operator
21
22from pkgcore.config import ConfigHint
23from pkgcore.util.compatibility import any
24from pkgcore.util.demandload import demandload
25demandload(globals(), "logging re itertools")
26
27repository_feed = "repo"
28category_feed = "cat"
29package_feed = "cat/pkg"
30versioned_feed = "cat/pkg-ver"
31ebuild_feed = "cat/pkg-ver+text"
32
33# The plugger needs to be able to compare those and know the highest one.
34version_scope, package_scope, category_scope, repository_scope = range(4)
35max_scope = repository_scope
36
37
38class Addon(object):
39
40    """Base class for extra functionality for pcheck other than a check.
41
42    The checkers can depend on one or more of these. They will get
43    called at various points where they can extend pcheck (if any
44    active checks depend on the addon).
45
46    These methods are not part of the checker interface because that
47    would mean addon functionality shared by checkers would run twice.
48    They are not plugins because they do not do anything useful if no
49    checker depending on them is active.
50
51    This interface is not finished. Expect it to grow more methods
52    (but if not overridden they will be no-ops).
53
54    @cvar required_addons: sequence of addons this one depends on.
55    """
56
57    required_addons = ()
58    known_results = []
59
60    def __init__(self, options, *args):
61        """Initialize.
62
63        An instance of every addon in required_addons is passed as extra arg.
64
65        @param options: the optparse values.
66        """
67        self.options = options
68
69    @staticmethod
70    def mangle_option_parser(parser):
71        """Add extra options and/or groups to the option parser.
72
73        This hook is always triggered, even if the checker is not
74        activated (because it runs before the commandline is parsed).
75
76        @param parser: an C{OptionParser} instance.
77        """
78
79    @staticmethod
80    def check_values(values):
81        """Postprocess the optparse values.
82
83        Should raise C{optparse.OptionValueError} on failure.
84
85        This is only called for addons that are enabled, but before
86        they are instantiated.
87        """
88
89
90class set_documentation(type):
91    def __new__(cls, name, bases, d):
92        if "__doc__" in d:
93            d.setdefault("documentation", d["__doc__"])
94        return type.__new__(cls, name, bases, d)
95
96class Template(Addon):
97
98    """Base template for a check."""
99
100    __metaclass__ = set_documentation
101
102    scope = 0
103    # The plugger sorts based on this. Should be left alone except for
104    # weird pseudo-checks like the cache wiper that influence other checks.
105    priority = 0
106
107    def start(self):
108        """Do startup here."""
109
110    def feed(self, item, reporter):
111        raise NotImplementedError
112
113    def finish(self, reporter):
114        """Do cleanup and omit final results here."""
115
116
117class Transform(object):
118
119    """Base class for a feed type transformer.
120
121    @cvar source: start type
122    @cvar dest: destination type
123    @cvar scope: minimun scope
124    @cvar cost: cost
125    """
126
127    def __init__(self, child):
128        self.child = child
129
130    def start(self):
131        """Startup."""
132        self.child.start()
133
134    def feed(self, item, reporter):
135        raise NotImplementedError
136
137    def finish(self, reporter):
138        """Clean up."""
139        self.child.finish(reporter)
140
141    def __repr__(self):
142        return '%s(%r)' % (self.__class__.__name__, self.child)
143
144    def finish(self, reporter):
145        pass
146
147
148def _collect_checks(obj):
149    if isinstance(obj, Transform):
150        i = collect_checks(obj.child)
151    elif isinstance(obj, CheckRunner):
152        i = itertools.chain(*map(collect_checks, obj.checks))
153    elif isinstance(obj, Addon):
154        i = [obj]
155    else:
156        i = itertools.chain(*map(collect_checks, i))
157    for x in i:
158        yield x
159
160def collect_checks(obj):
161    return set(_collect_checks(obj))
162
163def collect_checks_classes(obj):
164    return set(x.__class__ for x in collect_checks(obj))
165
166class Result(object):
167
168    __metaclass__ = set_documentation
169
170    __slots__ = ()
171
172    def __str__(self):
173        try:
174            return self.short_desc
175        except NotImplementedError:
176            return "result from %s" % self.__class__.__name__
177   
178    @property
179    def short_desc(self):
180        raise NotImplementedError
181
182    @property
183    def long_desc(self):
184        return self.short_desc
185   
186    def _store_cp(self, pkg):
187        self.category = pkg.category
188        self.package = pkg.package
189   
190    def _store_cpv(self, pkg):
191        self._store_cp(pkg)
192        self.version = pkg.fullver
193
194    def __getstate__(self):
195        if hasattr(self, "__slots__"):
196            try:
197                return dict((k, getattr(self, k)) for k in self.__slots__)
198            except AttributeError, a:
199                # rethrow so we at least know the class
200                raise AttributeError(self.__class__, str(a))
201        return object.__getstate__(self)
202   
203    def __setstate__(self, data):
204        for k, v in data.iteritems():
205            setattr(self, k, v)
206
207
208class Reporter(object):
209
210    def add_report(self, result):
211        raise NotImplementedError(self.add_report)
212
213    def start(self):
214        pass
215
216    def start_check(self, source, target):
217        pass
218   
219    def end_check(self):
220        pass
221
222    def finish(self):
223        pass
224
225
226def convert_check_filter(tok):
227    """Convert an input string into a filter function.
228
229    The filter function accepts a qualified python identifier string
230    and returns a bool.
231
232    The input can be a regexp or a simple string. A simple string must
233    match a component of the qualified name exactly. A regexp is
234    matched against the entire qualified name.
235
236    Matches are case-insensitive.
237
238    Examples::
239
240      convert_check_filter('foo')('a.foo.b') == True
241      convert_check_filter('foo')('a.foobar') == False
242      convert_check_filter('foo.*')('a.foobar') == False
243      convert_check_filter('foo.*')('foobar') == True
244    """
245    tok = tok.lower()
246    if '+' in tok or '*' in tok:
247        return re.compile(tok, re.I).match
248    else:
249        toklist = tok.split('.')
250        def func(name):
251            chunks = name.lower().split('.')
252            if len(toklist) > len(chunks):
253                return False
254            for i in xrange(len(chunks)):
255                if chunks[i:i+len(toklist)] == toklist:
256                    return True
257            return False
258        return func
259
260
261class _CheckSet(object):
262
263    """Run only listed checks."""
264
265    # No config hint here since this one is abstract.
266
267    def __init__(self, patterns):
268        self.patterns = list(convert_check_filter(pat) for pat in patterns)
269
270class Whitelist(_CheckSet):
271
272    """Only run checks matching one of the provided patterns."""
273
274    pkgcore_config_type = ConfigHint(
275        {'patterns': 'list'}, typename='pcheck_checkset')
276
277    def filter(self, checks):
278        return list(
279            c for c in checks
280            if any(f('%s.%s' % (c.__module__, c.__name__))
281                   for f in self.patterns))
282
283class Blacklist(_CheckSet):
284
285    """Only run checks not matching any of the provided patterns."""
286
287    pkgcore_config_type = ConfigHint(
288        {'patterns': 'list'}, typename='pcheck_checkset')
289
290    def filter(self, checks):
291        return list(
292            c for c in checks
293            if not any(f('%s.%s' % (c.__module__, c.__name__))
294                       for f in self.patterns))
295
296
297class Suite(object):
298
299    pkgcore_config_type = ConfigHint({
300            'target_repo': 'ref:repo', 'src_repo': 'ref:repo',
301            'checkset': 'ref:pcheck_checkset'}, typename='pcheck_suite')
302
303    def __init__(self, target_repo, checkset=None, src_repo=None):
304        self.target_repo = target_repo
305        self.checkset = checkset
306        self.src_repo = src_repo
307
308
309class CheckRunner(object):
310
311    def __init__(self, checks):
312        self.checks = checks
313
314    def start(self):
315        for check in self.checks:
316            # Intentionally not catching and logging exceptions:
317            # if we fail this early we may as well abort.
318            check.start()
319
320    def feed(self, item, reporter):
321        for check in self.checks:
322            try:
323                check.feed(item, reporter)
324            except (KeyboardInterrupt, SystemExit):
325                raise
326            except Exception:
327                logging.exception('check %r raised', check)
328
329    def finish(self, reporter):
330        for check in self.checks:
331            try:
332                check.finish(reporter)
333            except Exception:
334                logging.exception('finishing check %r failed', check)
335
336    # The plugger tests use these.
337    def __eq__(self, other):
338        return self.__class__ is other.__class__ and \
339            frozenset(self.checks) == frozenset(other.checks)
340
341    def __ne__(self, other):
342        return not self == other
343
344    def __hash__(self):
345        return hash(frozenset(self.checks))
346
347    def __repr__(self):
348        return '%s(%s)' % (self.__class__.__name__, ', '.join(sorted(
349                    str(check) for check in self.checks)))
350
351
352def plug(sinks, transforms, sources, debug=None):
353    """Plug together a pipeline.
354
355    This tries to return a single pipeline if possible (even if it is
356    more "expensive" than using separate pipelines). If more than one
357    pipeline is needed it does not try to minimize the number.
358
359    @param sinks: Sequence of check instances.
360    @param transforms: Sequence of transform classes.
361    @param sources: Sequence of source instances.
362    @param debug: A logging function or C{None}.
363    @returns: a sequence of sinks that are unreachable (out of scope or
364        missing sources/transforms of the right type),
365        a sequence of (source, consumer) tuples.
366    """
367
368    # This is not optimized to deal with huge numbers of sinks,
369    # sources and transforms, but that should not matter (although it
370    # may be necessary to handle a lot of sinks a bit better at some
371    # point, which should be fairly easy since we only care about
372    # their type and scope).
373
374    assert sinks
375
376    feed_to_transforms = {}
377    for transform in transforms:
378        feed_to_transforms.setdefault(transform.source, []).append(transform)
379
380    # Map from typename to best scope
381    best_scope = {}
382    for source in sources:
383        # (not particularly clever, if we get a ton of sources this
384        # should be optimized to do less duplicate work).
385        local_best_scope = {}
386        reachable = set()
387        todo = set([source.feed_type])
388        while todo:
389            feed_type = todo.pop()
390            reachable.add(feed_type)
391            for transform in feed_to_transforms.get(feed_type, ()):
392                if transform.scope <= source.scope and \
393                        transform.dest not in reachable:
394                    todo.add(transform.dest)
395        for feed_type in reachable:
396            scope = best_scope.get(feed_type)
397            if scope is None or scope < source.scope:
398                best_scope[feed_type] = source.scope
399
400    # Throw out unreachable sinks.
401    good_sinks = []
402    bad_sinks = []
403    for sink in sinks:
404        scope = best_scope.get(sink.feed_type)
405        if scope is None or sink.scope > scope:
406            bad_sinks.append(sink)
407        else:
408            good_sinks.append(sink)
409
410    if not good_sinks:
411        # No point in continuing.
412        return bad_sinks, ()
413
414    # Throw out all sources with a scope lower than the least required scope.
415    # Does not check transform requirements, may not be very useful.
416    lowest_required_scope = min(sink.scope for sink in good_sinks)
417    highest_required_scope = max(sink.scope for sink in good_sinks)
418    sources = list(s for s in sources if s.scope >= lowest_required_scope)
419    if not sources:
420        # No usable sources, abort.
421        return bad_sinks + good_sinks, ()
422
423    # All types we need to reach.
424    sink_types = set(sink.feed_type for sink in good_sinks)
425
426    # Map from scope, source typename to cheapest source.
427    source_map = {}
428    for new_source in sources:
429        current_source = source_map.get((new_source.scope,
430                                         new_source.feed_type))
431        if current_source is None or current_source.cost > new_source.cost:
432            source_map[new_source.scope, new_source.feed_type] = new_source
433
434    # Tuples of (visited_types, source, transforms, price)
435    pipes = set()
436    unprocessed = set(
437        (frozenset((source.feed_type,)), source, frozenset(), source.cost)
438        for source in source_map.itervalues())
439    if debug is not None:
440        for pipe in unprocessed:
441            debug('initial: %r', pipe)
442
443    # If we find a single pipeline driving all sinks we want to use it.
444    # List of tuples of source, transforms.
445    pipes_to_run = None
446    best_cost = None
447    while unprocessed:
448        next = unprocessed.pop()
449        if next in pipes:
450            continue
451        pipes.add(next)
452        visited, source, trans, cost = next
453        if visited >= sink_types:
454            # Already reaches all sink types. Check if it is usable as
455            # single pipeline:
456            if best_cost is None or cost < best_cost:
457                pipes_to_run = [(source, trans)]
458                best_cost = cost
459            # No point in growing this further: it already reaches everything.
460            continue
461        if best_cost is not None and best_cost <= cost:
462            # No point in growing this further.
463            continue
464        for transform in transforms:
465            if source.scope >= transform.scope and \
466                    transform.source in visited and \
467                    transform.dest not in visited:
468                unprocessed.add((
469                        visited.union((transform.dest,)), source,
470                        trans.union((transform,)), cost + transform.cost))
471                if debug is not None:
472                    debug(
473                        'growing %r for %r with %r', trans, source, transform)
474
475    if pipes_to_run is None:
476        # No single pipe will drive everything, try combining pipes.
477        # This is pretty stupid but effective. Map sources to
478        # pipelines they drive, try combinations of sources (using a
479        # source more than once in a combination makes no sense since
480        # we also have the "combined" pipeline in pipes).
481        source_to_pipes = {}
482        for visited, source, trans, cost in pipes:
483            source_to_pipes.setdefault(source, []).append(
484                (visited, trans, cost))
485        unprocessed = set(
486            (visited, frozenset([source]), ((source, trans),), cost)
487            for visited, source, trans, cost in pipes)
488        done = set()
489        while unprocessed:
490            next = unprocessed.pop()
491            if next in done:
492                continue
493            done.add(next)
494            visited, sources, seq, cost = next
495            if visited >= sink_types:
496                # This combination reaches everything.
497                if best_cost is None or cost < best_cost:
498                    pipes_to_run = seq
499                    best_cost = cost
500                # No point in growing this further.
501            if best_cost is not None and best_cost <= cost:
502                # No point in growing this further.
503                continue
504            for source, source_pipes in source_to_pipes.iteritems():
505                if source not in sources:
506                    for new_visited, trans, new_cost in source_pipes:
507                        unprocessed.add((
508                                visited.union(new_visited),
509                                sources.union([source]),
510                                seq + ((source, trans),),
511                                cost + new_cost))
512
513    # Just an assert since unreachable sinks should have been thrown away.
514    assert pipes_to_run, 'did not find a solution?'
515
516    good_sinks.sort(key=operator.attrgetter('priority'))
517
518    def build_transform(scope, feed_type, transforms):
519        children = list(
520            # Note this relies on the cheapest pipe not having
521            # any "loops" in its transforms.
522            trans(build_transform(scope, trans.dest, transforms))
523            for trans in transforms
524            if trans.source == feed_type and trans.scope <= scope)
525        # Hacky: we modify this in place.
526        for i in reversed(xrange(len(good_sinks))):
527            sink = good_sinks[i]
528            if sink.feed_type == feed_type and sink.scope <= source.scope:
529                children.append(sink)
530                del good_sinks[i]
531        return CheckRunner(children)
532
533    result = list(
534        (source, build_transform(source.scope, source.feed_type, transforms))
535        for source, transforms in pipes_to_run)
536
537    assert not good_sinks, 'sinks left: %r' % (good_sinks,)
538    return bad_sinks, result
Note: See TracBrowser for help on using the browser.