#!/www/python/bin/python
"""$URL: svn+ssh://svn.mems-exchange.org/repos/trunk/dulcinea/bin/check_durus.py $
$Id: check_durus.py 25796 2004-12-16 17:48:26Z dbinger $
"""
import sys
from sets import Set
from dulcinea.spec import get_spec_problems, mapping, sequence, any, \
     get_specs, nspec, get_spec_doc
from durus.connection import Connection
from durus.file_storage import FileStorage
from durus.client_storage import ClientStorage
from durus.storage import gen_oid_class, get_reference_index
from durus.storage_server  import DEFAULT_PORT, DEFAULT_HOST

class ObjectLocator:

    def __init__(self, connection):
        self.connection = connection
        self.reference_index = get_reference_index(connection.get_storage())

    def where_is(self, obj):
        root = self.connection.get_root()
        def locate(obj, seen):
            if len(seen) > 50:
                return '...'
            if obj is root:
                return 'root'
            oid = obj._p_oid
            for referring_oid in self.reference_index[oid]:
                if referring_oid not in seen:
                    seen.add(referring_oid)
                    break
            else:
                return repr(obj) # give up
            referring = self.connection.get(referring_oid)
            ref = locate(referring, seen)
            for key, value in vars(referring).iteritems():
                if obj is value:
                    return "%s.%s" % (ref, key)
                try:
                    for k, v in value.iteritems():
                        if obj is v:
                            return "%s[%r]" % (ref, k)
                except AttributeError:
                    pass
                try:
                    for k, v in enumerate(value):
                        if obj is v:
                            return "%s[%r]" % (ref, k)
                except TypeError:
                    pass
            return '%s ... ' % ref
        return locate(obj, Set())

def gen_class_name_oids(storage):
    oids_by_class_name = {}
    for oid, class_name in gen_oid_class(storage):
        oids_by_class_name.setdefault(class_name, []).append(oid)
    class_name_oids = oids_by_class_name.items()
    class_name_oids.sort()
    for item in class_name_oids:
        yield item

def format_specs(klass):
    if klass.__bases__:
        bases = '(%s):' % ', '.join(
            [b.__name__ for b in klass.__bases__])
    else:
        bases = ":"
    s = "class %s%s\n    " % (klass.__name__, bases)
    s += get_spec_doc(klass).replace('\n', '\n    ') or 'pass'
    s += "\n\n"
    return s

def check_durus(file, host, port, schema, klasses):
    if file:
        storage = FileStorage(file, readonly=True)
    else:
        storage = ClientStorage(host=host, port=port)
    connection = Connection(storage)
    where_is = ObjectLocator(connection).where_is
    try:
        import grouch
        _unused = grouch # so that import checker does not complain        
        get_specs = Grouchy(connection, klasses).get_specs
    except ImportError:
        pass
    if schema:
        schema_output = open(schema, 'w')
        written = Set()
    num_instances_checked = 0
    for class_name, oids in gen_class_name_oids(storage):
        if klasses and class_name not in klasses:
            continue
        print "%6d %s" % (len(oids), class_name)
        for oid in oids:
            obj = connection.get(oid)
            specs = get_specs(obj)
            if schema and obj.__class__ not in written:
                written.add(obj.__class__)
                schema_output.write(format_specs(obj.__class__))
            num_instances_checked += 1
            problems = get_spec_problems(obj, specs=specs)
            if problems:
                print
                print where_is(obj)
                for problem in problems:
                    for line in problem.splitlines():
                        if len(line) > 200:
                            line = line[:200] + ' ...'
                        print ' ', line
                break
            obj._p_set_status_ghost()
    print num_instances_checked, 'instances checked.'

class Grouchy:

    def __init__(self, connection, klass_names):
        from grouch.type_parser import TypeScanner, TypeParser
        self.scanner = TypeScanner()
        self.parser = TypeParser(schema=self)
        self.known_specs = {}
        # Build a base environment to use for evaluating typestrings.
        env = __builtins__.__dict__.copy()
        classes = [
            connection.get(oids[0]).__class__
            for name, oids in gen_class_name_oids(connection.get_storage())
            if not klass_names or name in klass_names]
        # The following line should go away.
        import durus._persistent; sys.modules['_persistent'] = durus._persistent
        while classes:
            klass = classes.pop()
            env[klass.__name__] = klass
            env["%s.%s" % (klass.__module__, klass.__name__)] = klass
            for value in sys.modules[klass.__module__].__dict__.itervalues():
                if hasattr(value, '__bases__') and not value.__name__ in env:
                    classes.append(value)
            for base in getattr(klass, '__mro__', klass.__bases__):
                if base.__name__ not in env:
                    classes.append(base)
        env.update(sys.modules)
        import string
        _unused = string # so that import checker does not complain
        env['string'] = str
        import dulcinea.spec
        env['boolean'] = dulcinea.spec.boolean
        env['any'] = dulcinea.spec.anything
        from types import ClassType, TypeType
        env['class'] = any(ClassType, TypeType)
        self.env = env
        self.klass_env = env

    def get_specs(self, obj):
        from grouch.schema_util import parse_docstring_helper
        def set_spec(klass, name, typespec):
            is_name = name + '_is'
            if is_name not in klass.__dict__:
                try:
                    setattr(klass, is_name, nspec(typespec)) # allows None.
                except TypeError, e:
                    return False
            return True
        def get_class_specs(klass):
            errors = []
            attrs = []
            if klass in __builtins__.__dict__.itervalues():
                self.known_specs[klass] = {}
                return {}
            if klass in self.known_specs:
                return self.known_specs[klass]
            try:
                env = self.env.copy()
                env.update(sys.modules[klass.__module__].__dict__)
                env.update(klass.__dict__)
                self.klass_env = env
                self.klass_env['string'] = str
                try:
                    if klass.__doc__:
                        errors, attrs = parse_docstring_helper(
                            self, klass.__doc__,
                            klass.__name__)
                except Exception, e:
                    if 'Instance attributes:' not in str(e):
                        e.args += ("in %s" % klass.__name__,)
                        raise
                for name, type in attrs:
                    if not set_spec(klass, name, type):
                        self.known_specs[klass] = dict(attrs)
                        break
                for b in klass.__bases__:
                    base_specs = get_class_specs(b)
                    if base_specs != get_specs(b):
                        for key, val in base_specs.iteritems():
                            if not set_spec(klass, key, val):
                                    break
            except ValueError, exc:
                errors = [str(exc)]

            if errors:
                print >>sys.stderr, '\nWARNING: '.join([''] + errors),

            if klass not in self.known_specs:
                self.known_specs[klass] = get_specs(klass)
            return get_class_specs(klass)
        specs = get_class_specs(obj.__class__)
        return specs

    def parse_type(self, typestr):
        self.current_typestr = typestr
        tokens = self.scanner.tokenize(typestr)
        return self.parser.parse(tokens)

    def get_type(self, s):
        if type(s) is type:
            return s
        if type(s) is str:
            if s in self.klass_env:
                return self.klass_env.get(s)
        try:
            return eval(s, self.klass_env)
        except Exception, e:
            e.args += ("Can't parse: %r in %r" % (s, self.current_typestr),)
            raise

    def make_list_type(self, *args):
        return list(args)

    def make_tuple_type(self, element_types, extended):
        assert not extended
        return tuple(element_types)

    def make_dictionary_type(self, key, value):
        return {key:value}

    def make_union_type(self, typelist):
        return any(*typelist)

    def make_set_type(self, element_type):
        return sequence(element_type, Set)

    def make_instance_container_type(self, name, element_type):
        if type(name) is not str:
            name = name.name # grouch.type_parser.Name
        if type(element_type) is dict:
            container = mapping
        else:
            container = sequence
            element_type = element_type[0]
        if name in self.env:
            return container(element_type, self.env.get(name))
        return container(element_type)

def check_durus_main():
    from optparse import OptionParser
    parser = OptionParser()
    parser.set_description("Searches for spec anomolies in a durus database.")
    parser.add_option(
        '--file', dest="file", default=None,
        help="If this is not given, the storage is through a Durus server.")
    parser.add_option(
        '--port', dest="port", default=DEFAULT_PORT,
        type="int",
        help="Port the server is on. (default=%s)" % DEFAULT_PORT)
    parser.add_option(
        '--host', dest="host", default=DEFAULT_HOST,
        help="Host of the server. (default=%s)" % DEFAULT_HOST)
    parser.add_option(
        '--schema', dest="schema", default=None,
        help="If given, a schema will be written to this file")
    parser.add_option(
        '--class', dest="classes", default=[], action="append",
        help="If given, limit study to this class.")
    (options, args) = parser.parse_args()
    check_durus(options.file, options.host, options.port, options.schema,
                options.classes)

if __name__ == '__main__':
    check_durus_main()

