"""$URL: svn+ssh://svn/repos/trunk/grouch/lib/context.py $ $Id: context.py 24750 2004-07-21 15:26:51Z dbinger $ Provides the TypecheckContext class, instances of which are carried around throughout a type-checking traversal of an entire obejct graph. """ import sys, string 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