"""grouch.context

Provides the TypecheckContext class, instances of which are carried around
throughout a type-checking traversal of an entire obejct graph.
"""

# created 2001/02, GPW
#
# Copyright (c) 2001 CNRI.  All Rights Reserved.

__revision__ = "$Id: context.py,v 1.13 2001/09/05 14:21:23 gward Exp $"

import sys, string
from copy import copy


class TypecheckContext:
    """
    Stores preferences, context, and results of a type-checking traversal
    over an object graph.

    Instance attributes:
      context : [(action:string, label:string)]
        list of tuples that record each step through the object graph.  The
        tuples look like eg. ('item', 3) for looking up item 3 of a
        sequence, or ('attr', 'foo') for looking up attribute 'foo' of an
        instance.  The first tuple in the list might look like ('name',
        'start') to distinguish it from an attribute lookup, which it
        probably isn't.  The context list is used to generate meaningful
        error messages from deep within an object graph.
      object_seen : { int : boolean }
        map object IDs to true values for objects that have already
        been visited and checked
      mode : string   # one of 'memory', 'zodb'
        what type of object graph we're traversing; currently used to
        know how to determine object IDs
      errors : [ (context, message) ]
        list of typechecking errors.  'context' is a context list (just a
        copy of the 'context' attribute at the point where the error is
        seen).  'message' is a human-readable string describing the type
        error.
      report_errors : boolean = true
        if true, errors will be reported (printed to stderr) as soon
        as they are seen.  (Errors are always stored in the 'errors'
        list for future reference.)
      visit_counts : { metatype:string : { extra:string : count:int } }
        counts of the types of objects visited in a type-checking
        traversal.  The keys of 'visit_counts' are strings like
        "atomic" or "instance" -- the major "meta-types" of the
        Grouch type system.  The values are dictionaries mapping
        specific type names (eg. "int" or "string" are specific atomic
        types; "Foo" and "Bar" might be specific instance types)
        to the number of objects of that type visited.  Certain
        metatypes (eg. boolean, any) won't have any extra type info,
        so their sub-dictionaries will be singleton whose sole key
        is None.
    """

    def __init__ (self, mode='memory', start_name=None, report_errors=1):
        if mode not in ('memory', 'zodb'):
            raise ValueError, "unknown mode: %s" % mode

        self.context = []
        self.start_context(start_name)

        self.object_seen = {}
        self.mode = mode
        self.errors = []
        self.report_errors = report_errors
        self.visit_counts = {}

        if self.mode == 'zodb':
            self._id = self._zodb_id
        else:
            self._id = id

    def _zodb_id (self, object):
        try:
            return object._p_oid
        except AttributeError:
            return id(object)           # not great, but better than nothing

    def start_context (self, start_name):
        del self.context[:]
        if start_name:
            self.context.append(('name', start_name))

    def reset (self, start_name=None):
        del self.context[:]
        self.object_seen.clear()
        del self.errors[:]
        self.start_context(start_name)
        self.visit_counts.clear()


    def has_seen (self, object):
        return self.object_seen.get(self._id(object))

    def set_seen (self, object, seen=1):
        self.object_seen[self._id(object)] = seen

    def check_seen (self, object):
        """If 'object' has already been visited, return true; otherwise,
        record that the object has been visited and return false.
        """
        oid = self._id(object)
        if self.object_seen.get(oid):
            return 1
        else:
            self.object_seen[oid] = 1
            return 0

    def push_context (self, action, label):
        """Descend a level in the object graph and append a new
        entry to the 'context' list.
        """
        if action not in ('attr', 'item', 'dkey', 'dval'):
            raise ValueError, "invalid 'action': must be 'attr' or 'item'"
        self.context.append((action, label))

    def format_context (self):
        context_l = []
        for (action, label) in self.context:
            if action == 'name':
                context_l.append(label)
            elif action == 'attr':
                if context_l:
                    context_l.append('.' + label)
                else:
                    context_l.append(label)
            elif action in ('item', 'dkey', 'dval'):
                context_l.append('[%s]' % `label`)
                if action == 'dkey':
                    context_l.append(' (key)')

        return string.join(context_l, '')

    def replace_context (self, action, label):
        del self.context[-1]
        self.push_context(action, label)

    def pop_context (self):
        if len(self.context) < 1:
            raise RuntimeError, "can't pop context: already at top"
        del self.context[-1]


    def add_error (self, message):
        context = self.format_context()
        self.errors.append((context, message))
        if self.report_errors:
            self.write_error(sys.stderr, self.errors[-1])

    def forget_errors (self, num=1):
        if num > 0:
            del self.errors[-num:len(self.errors)]
            
    def format_errors (self):
        errors = []
        for (context, message) in self.errors:
            if context:
                errors.append("%s: %s" % (context, message))
            else:
                errors.append(message)
        return errors

    def write_error (self, file, error):
        (context, message) = error
        if context:
            file.write(context + ":\n")
            file.write("  " + message + "\n")
        else:
            file.write(message + "\n")

    def write_errors (self, file):
        for error in self.errors:
            self.write_error(file, error)

    def num_errors (self):
        return len(self.errors)


    def count_object (self, type, extra_type=None):
        typename = type.metatype
        counts = self.visit_counts.get(typename)
        if counts is None:
            counts = self.visit_counts[typename] = {}
        if counts.has_key(extra_type):
            counts[extra_type] += 1
        else:
            counts[extra_type] = 1

    def uncount_object (self, type, extra_type=None):
        typename = type.metatype
        if self.visit_counts[typename][extra_type] == 1:
            del self.visit_counts[typename][extra_type]
            if len(self.visit_counts[typename]) == 0:
                del self.visit_counts[typename]
        else:
            self.visit_counts[typename][extra_type] -= 1

    def report_counts (self, file=None):
        if file is None:
            file = sys.stdout
        metatypes = self.visit_counts.keys()
        metatypes.sort()

        for metatype in metatypes:
            counts = self.visit_counts[metatype]
            if len(counts) == 1 and counts.keys()[0] is None:
                file.write("%-10s%-55s%10d\n" %
                           (metatype, "", counts[None]))
            else:
                file.write("%s:\n" % metatype)
                types = counts.keys()
                types.sort()
                for type in types:
                    file.write("%2s%-63s%10d\n" %
                               ("", type, counts[type]))

        print "objects visited:", len(self.object_seen)

# class TypecheckContext
