"""$URL: svn+ssh://svn/repos/trunk/grouch/lib/valuetype.py $ $Id: valuetype.py 24981 2004-08-26 19:56:31Z dbinger $ Provides the ValueType class and its subclasses, which together represent the Python type system as interpreted by Grouch. """ import string import types from sets import Set from copy import copy from grouch.util import \ isinstance, is_instance_object, \ get_class_object, \ get_full_classname, get_type_name # XXX Type-checking issues yet to be resolved # # * if an "any" type has 'allow_none' false, does it allow None? # eg. if 'a' is declared "a : any notnone", can 'a' be None? # * ditto for boolean types: # if 'f' is decalred "f : boolean notnone", can 'f' be None? # * what does an empty/uninitialized union type mean? no values at # all are valid? perhaps only None should be allowed? class ValueType: """ Instance attributes: schema : ObjectSchema the object schema (atomic types, aliases, and class definitions) relative to which we should interpret this type allow_none : boolean = true are values allowed to be None? """ # class attribute 'metatype' must be defined by each subclass metatype = None def __init__ (self, schema): self.schema = schema self.allow_none = 1 assert self.__class__ is not ValueType,\ "ValueType is an abstract class" assert self.metatype is not None,\ "metatype must be set by subclass" # Subclasses must provide '__str__()' def __repr__ (self): return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self) # -- Accessors/modifiers ------------------------------------------- def get_metatype (self): return self.metatype # really a class attribute! # -- Predicates ---------------------------------------------------- # XXX might be cleaner (and more extensible) to just "return 0" # everywhere here, and require subclasses to override and # "return 1" where applicable def is_atomic_type (self): return self.metatype == "atomic" def is_instance_type (self): return self.metatype in ("instance", "instance-container") def is_plain_instance_type (self): return self.metatype == "instance" def is_container_type (self): return self.metatype in ("container", "instance-container") def is_plain_container_type (self): return self.metatype == "container" def is_instance_container_type (self): return self.metatype == "instance-container" def is_union_type (self): return self.metatype == "union" def is_any_type (self): return self.metatype == "any" def is_boolean_type (self): return self.metatype == "boolean" def is_alias_type (self): return self.metatype == "alias" # -- Type-checking ------------------------------------------------- def simple_check (self, value): raise NotImplementedError, "subclasses must implement" def check_value (self, value, context): raise NotImplementedError, "subclasses must implement" def check_none_ok (self, value): return (value is None and self.allow_none) def check_unexpected_instance (self, value, context, allow_foreign=0): if not is_instance_object(value): return 1 try: itype = self.schema.make_instance_type(value.__class__) except ValueError, msg: # XXX ugh, should probably have some custom exceptions msg = str(msg) # just in case if not msg.startswith("class not in schema"): raise if allow_foreign: return 1 else: context.add_error("found instance of class not in schema: %s" % get_full_classname(value.__class__)) return 0 else: return itype.check_value(value, context) def count_object (self, context): context.count_object(self) def uncount_object (self, context): context.uncount_object(self) class AtomicType (ValueType): """ Instance attributes: type : type the type object that values must match type_name : string the name of the atomic type """ metatype = "atomic" def __init__ (self, schema): ValueType.__init__(self, schema) self.type = None self.type_name = None def __str__ (self): return self.type_name or "*no type*" # -- Pickle support ------------------------------------------------ # (needed because you can't pickle type objects) # Pickling AtomicType instances is a bit tricky, because each # AtomicType carries around a type object in its 'type' attribute, # but you can't pickle type objects. Thus we have to get rid of the # type objects at pickle-time, but we can't reconstitute them at # unpickle-time, because we need the enclosing ObjectSchema to # lookup the type name from the type object. Depending on what # order things were pickled in, our schema may or may not be # available or complete, so we can't depend on it being there. # # The solution is to leave the 'type' attribute None (which is what # it's set to at pickle-time), and then lookup the type object in # the schema only when we need it, ie. when either 'get_type()' or # 'check_value()' is called. def __getstate__ (self): if self.type is None: return self.__dict__ else: dict = copy(self.__dict__) dict['type'] = None return dict # -- Accessors/modifiers ------------------------------------------- def set_type (self, tipe): tipe = self.schema.get_atomic_type_object(tipe) self.type = tipe self.type_name = self.schema.get_atomic_type_name(tipe) def get_type (self): if self.type is None: self.type = self.schema.get_atomic_type_object(self.type_name) return self.type # -- Type-checking methods ----------------------------------------- def simple_check (self, value): vtype = type(value) if self.type is None: # first use after unpickling? self.type = self.schema.get_atomic_type_object(self.type_name) return (vtype is self.type) or self.check_none_ok(value) def check_value (self, value, context): self.count_object(context) if self.simple_check(value): return 1 else: context.add_error("expected %s, got %s" % (str(self), describe_value(value))) self.check_unexpected_instance(value, context) return 0 def count_object (self, context): context.count_object(self, self.type_name) def uncount_object (self, context): context.uncount_object(self, self.type_name) # class AtomicType # XXX name collision w/ standard 'types' modules! class InstanceType (ValueType): """ Instance attributes: klass_name : string the (fully-qualified) name of the class klass_definition : ClassDefinition the definition of this class in the object schema (defines what attributes are allowed/expected and what their types are) klass : class a class object; values must be instances of this class XXX klass and klass_definition might be obsolete; they are now assigned only when needed, ie. in check_value() """ metatype = "instance" def __init__ (self, schema): ValueType.__init__(self, schema) self.klass_name = None self.klass_definition = None self.klass = None def __str__ (self): return self.klass_name or "*no class*" def set_class_name (self, klass_name): if type(klass_name) is not types.StringType: raise TypeError, "not a string: %r" % klass_name self.klass_name = klass_name def get_class_name (self): return self.klass_name # Helper methods for 'check_value()' def _find_classdef (self, value, context): klass = value.__class__ if klass is self.klass: klass_def = self.klass_definition else: klass_def = self.schema.get_class_definition(klass) if not klass_def: context.add_error("expected instance of %s, " "got instance of unknown subclass %s" % (self.klass, get_full_classname(klass))) return klass_def def _check_attrs (self, value, klass_def, context): #print "checking " + repr(value) # First, make sure there are no unexpected attributes. (Doing this # first -- in the instance dictionary's hash order -- means the # *real* type-checking loop, below, is done according to the order # of attributes in the class definition, ie. in a predictable, # canonical order. This is a good thing, if only because it makes # writing tests easier.) if context.mode == "zodb": # prod ZODB into loading the object before relying on # the value of dir() dir(value) #print "activated %r" % value if hasattr(value, '_p_oid'): obj_id = "oid %r, id %x" % (value._p_oid, id(value)) else: obj_id = "id %x" % id(value) all_ok = 1 # Some classes don't provide # a __dict__ for their instances; treat that as an empty # instance attribute dictionary. if hasattr(value, '__dict__'): actual_dict = vars(value) else: actual_dict = {} for (actual_attr, val) in actual_dict.items(): if actual_attr.startswith('__') and actual_attr.endswith('__'): continue if context.mode == "zodb" and actual_attr.startswith("_v_"): continue if not klass_def.get_attribute_type(actual_attr): context.add_error("found unexpected attribute '%s': %s" % (actual_attr, describe_value(val))) context.push_context('attr', actual_attr) self.check_unexpected_instance(val, context) context.pop_context() all_ok = 0 # Now the real type-checking loop, in canonical (not hash) # order. By-the-by, we also check for missing attributes here. for expected_attr in klass_def.all_attrs: if actual_dict.has_key(expected_attr): val = actual_dict[expected_attr] expected_type = klass_def.get_attribute_type(expected_attr) context.push_context('attr', expected_attr) ok = expected_type.check_value(val, context) if not ok: all_ok = 0 context.pop_context() else: context.add_error("expected attribute '%s' not found" % expected_attr) all_ok = 0 if context.mode == "zodb" and hasattr(value, '_p_deactivate'): #print "deactivating %r" % value value._p_deactivate() return all_ok def simple_check (self, value): return (self.check_none_ok(value) or isinstance(value, get_class_object(self.klass_name))) def check_value (self, value, context): # XXX from Jim Fulton on zodb-dev@zope.org, about how to # reduce memory use of the type-checking script: # # You could modify your script to your script to "ghostify" the # objects, by calling _p_deactivate on them after you use them # to prevent the memory bloat. This is what the Zope "find" # facility does when searching for objects. # # I think this is the place to do it, but this method would need # a bit of rearrangement. if self.klass is None: self.klass = get_class_object(self.klass_name) if self.klass_definition is None: self.klass_definition = \ self.schema.get_class_definition(self.klass_name) self.count_object(context) if isinstance(value, self.klass): klass_def = self._find_classdef(value, context) if not klass_def: return 0 if context.check_seen(value): # No new errors to add here, so act as though the guts of # the object are fine. self.uncount_object(context) return 1 # XXX maybe the context should keep track of which # instances/containers had errors in them, so we can say # "second visit to object with errors" -- hmm, that'll only # work with instances, because a container might visited # twice with different declarations in force. #print "checking", repr(value) return self._check_attrs(value, klass_def, context) elif self.check_none_ok(value): return 1 else: context.add_error("expected instance of %s, got %s" % (self.klass, describe_value(value))) self.check_unexpected_instance(value, context) return 0 # if/elif/else # check_value () def count_object (self, context): context.count_object(self, self.klass_name) def uncount_object (self, context): context.uncount_object(self, self.klass_name) # class InstanceType class ContainerType (ValueType): """ Instance attributes: allow_empty : boolean = true only for container and instance-container types: are values allowed to be empty containers? """ metatype = "container" container_type = None # subclasses must set def __init__ (self, schema): assert self.__class__ is not ContainerType,\ "ContainerType is an abstract class" ValueType.__init__(self, schema) self.allow_empty = 1 # -- Type-checking methods ----------------------------------------- container_type_object = { "list" : types.ListType, "tuple" : types.TupleType, "dictionary" : types.DictionaryType, "set" : Set, } def simple_check (self, value): return (self.check_none_ok(value) or type(value) is self.container_type_object[self.container_type]) def check_value (self, value, context): self.count_object(context) if type(value) is self.container_type_object[self.container_type]: if len(value) == 0 and self.container_type != 'tuple': if not self.allow_empty: context.add_error("empty %s not allowed" % self.container_type) return 0 else: return 1 return self.check_contents(value, context) elif self.check_none_ok(value): return 1 else: context.add_error("expected %s, got %s" % (self.container_type, describe_value(value))) self.check_unexpected_instance(value, context) return 0 def count_object (self, context): context.count_object(self, self.container_type) def uncount_object (self, context): context.uncount_object(self, self.container_type) # class ContainerType class ListType (ContainerType): """ Instance attributes: element_type : ValueType the type that all elements of the list must be """ container_type = "list" def __init__ (self, schema): ContainerType.__init__(self, schema) self.element_type = None def __str__ (self): return "[%s]" % self.element_type def set_element_type (self, element_type): self.element_type = self.schema.get_type(element_type) set_list_type = set_element_type # backwards compat. def get_element_type (self): return self.element_type # -- Type-checking methods ----------------------------------------- def check_contents (self, value, context): i = 0 all_ok = 1 for item in value: context.push_context('item', i) ok = self.element_type.check_value(item, context) if not ok: all_ok = 0 context.pop_context() i = i + 1 return all_ok class TupleType (ContainerType): """ Instance attributes: element_types : [ValueType] the type of each "slot" in the tuple (values of this type must be tuples of the same length as 'element_types', and each element of the tuple must match the corresponding type in 'element_types' extended : boolean = false if true, this type is for "extended" tuples """ container_type = "tuple" def __init__ (self, schema): ContainerType.__init__(self, schema) self.element_types = [] self.extended = 0 def __str__ (self): types = self.element_types if self.extended: if len(types) == 1: return "(%s*,)" % types[0] else: types = map(str, types) return ("(%s, %s*)" % (string.join(types[0:-1], ", "), types[-1])) else: # regular tuple type if len(types) == 1: return "(%s,)" % types[0] else: types = map(str, types) return "(%s)" % string.join(types, ", ") def set_element_types (self, element_types): if type(element_types) not in (types.TupleType, types.ListType): raise TypeError, \ "not a sequence of type objects: %s" % `element_types` self.element_types = map(self.schema.get_type, element_types) def set_extended (self, extended): self.extended = extended set_tuple_type = set_element_types # backwards compat. def get_element_types (self): return self.element_types def is_extended (self): return self.extended def check_contents (self, value, context): types = self.element_types ext = self.extended # First, just check the length; if len(element_types) == N: # - regular tuples must be of length N # - extended tuples must be of length >= N-1 # (last item repeats 0 .. * times) if (not ext and len(value) != len(types)): context.add_error("expected tuple of length %d, " "got one of length %d" % (len(types), len(value))) all_ok = 0 elif (ext and len(value) < len(types)-1): context.add_error("expected tuple of at least length %d, " "got one of length %d" % (len(types)-1, len(value))) all_ok = 0 else: all_ok = 1 # Length is OK, so now check the actual elements if ext: # check the whole tuple n = len(value) else: # check only what we have types for n = min(len(value), len(types)) for i in range(n): context.push_context('item', i) if ext and i >= len(types): cur_type = types[-1] else: cur_type = types[i] if not cur_type.check_value(value[i], context): all_ok = 0 context.pop_context() return all_ok class DictionaryType (ContainerType): """ Instance attributes: key_type : ValueType value_type : ValueType """ container_type = "dictionary" def __init__ (self, schema): ContainerType.__init__(self, schema) self.key_type = None self.value_type = None def __str__ (self): return "{ %s : %s }" % (self.key_type, self.value_type) def set_key_type (self, key_type): self.key_type = self.schema.get_type(key_type) def set_value_type (self, value_type): self.value_type = self.schema.get_type(value_type) def set_item_types (self, key_type, value_type): self.key_type = self.schema.get_type(key_type) self.value_type = self.schema.get_type(value_type) set_dictionary_type = set_item_types # backwards compat. def get_key_type (self): return self.key_type def get_value_type (self): return self.value_type def get_item_types (self): return (self.key_type, self.value_type) def check_contents (self, value, context): all_ok = 1 for key in value.keys(): context.push_context('dkey', key) if not self.key_type.check_value(key, context): all_ok = 0 context.replace_context('dval', key) val = value[key] if not self.value_type.check_value(val, context): all_ok = 0 context.pop_context() return all_ok class SetType (ContainerType): """ Instance attributes: element_type : ValueType the type that all elements of the set must be """ container_type = "set" def __init__ (self, schema): ContainerType.__init__(self, schema) self.element_type = None def __str__ (self): return "{%s}" % self.element_type def set_element_type (self, element_type): self.element_type = self.schema.get_type(element_type) def get_element_type (self): return self.element_type # -- Type-checking methods ----------------------------------------- def check_contents (self, value, context): i = 0 all_ok = 1 for item in value: context.push_context('item', i) # XXX sets are unordered, fix? ok = self.element_type.check_value(item, context) if not ok: all_ok = 0 context.pop_context() i = i + 1 return all_ok class InstanceContainerType (InstanceType): """ Instance attributes: container_type : ContainerType """ metatype = "instance-container" def __init__ (self, schema): InstanceType.__init__(self, schema) self.container_type = None def __str__ (self): return "%s %s" % (self.klass_name, self.container_type) def set_container_type (self, container_type): if not isinstance(container_type, ContainerType): raise TypeError, \ "not a ContainerType instance: %s" % `container_type` self.container_type = container_type def get_container_type (self): return self.container_type def get_element_type (self): if not isinstance(self.container_type, ListType): raise RuntimeError, "not a list type" return self.container_type.get_element_type() def get_element_types (self): if not isinstance(self.container_type, TupleType): raise RuntimeError, "not a tuple type" return self.container_type.get_element_types() def get_item_types (self): if not isinstance(self.container_type, DictionaryType): raise RuntimeError, "not a dictionary type" return self.container_type.get_item_types() def simple_check (self, value): return (self.check_none_ok(value) or (self.container_type.simple_check(value) and InstanceType.simple_check(self, value))) def check_value (self, value, context): if self.check_none_ok(value): return 1 cont_ok = self.container_type.check_contents(value, context) inst_ok = InstanceType.check_value(self, value, context) return inst_ok and cont_ok class UnionType (ValueType): """ Instance attributes: union_types : [ValueType] a set of types; values must match one of them """ metatype = "union" def __init__ (self, schema): ValueType.__init__(self, schema) self.union_types = None def __str__ (self): types = map(str, self.union_types) return string.join(types, " | ") def set_union_types (self, union_types): if type(union_types) is not types.ListType: raise TypeError, "not a list of type objects: %s" % `union_types` union_types = map(self.schema.get_type, union_types) self.union_types = union_types def get_union_types (self): return self.union_types def simple_check (self, value): # XXX using sub-type's simple_check() here is not perfect: # consider what happens with a declaration like # x : [int] | [string] # ...as long as x is a list, either branch is OK, so we'll # assume x is a list of ints. If it's really a list of strings, # we'll barf unfairly all over it. But it's not enough to # just go down one level into the list; what if that declaration # were # x : [(int,string)] | [(string,int)] # ? How far down do you have to go to differentiate the # branches of a UnionType? I suspect this is a lost cause, # which is why I'm punting and leaving it obviously broken # (for the "[int] | [string]" case). for subtype in self.union_types: if subtype.simple_check(value): return subtype return None def check_value (self, value, context): self.count_object(context) subtype = self.simple_check(value) if subtype: ok = subtype.check_value(value, context) subtype.uncount_object(context) return ok else: types = self.union_types if len(types) == 0: prefix = "no valid values for empty union type" else: # Ugh, can forget about I18N of this code... tnames = map(str, types) if len(types) == 1: tlist = tnames[0] elif len(types) == 2: tlist = "%s or %s" % tuple(tnames) else: tlist = (string.join(tnames[0:-1], ", ") + ", or " + tnames[-1]) prefix = "expected " + tlist context.add_error(prefix + "; got %s" % describe_value(value)) self.check_unexpected_instance(value, context) return 0 # check_value () def count_object (self, context): context.count_object(self, str(self)) def uncount_object (self, context): context.uncount_object(self, str(self)) # class UnionType class AnyType (ValueType): """ Instance attributes: none allow_any_instance : boolean if true (the default), class instances of any class are allowed; otherwise, instances must be of classes known to the containing object schema. """ metatype = "any" def __init__ (self, schema): ValueType.__init__(self, schema) self.allow_any_instance = 1 def __str__ (self): return "any" def set_allow_any_instance (self, allow_any_instance): self.allow_any_instance = allow_any_instance def simple_check (self, value): return value is not None or self.allow_none def check_value (self, value, context): self.count_object(context) if self.simple_check(value): return self.check_unexpected_instance( value, context, allow_foreign=self.allow_any_instance) else: context.add_error("expected anything except None") return 0 class BooleanType (ValueType): """ Instance attributes: none """ metatype = "boolean" def __init__ (self, schema): ValueType.__init__(self, schema) def __str__ (self): return "boolean" def simple_check (self, value): return value in (0, 1, None) def check_value (self, value, context): self.count_object(context) if self.simple_check(value): return 1 else: context.add_error("expected a boolean, got %s" % describe_value(value)) self.check_unexpected_instance(value, context) return 0 class AliasType (ValueType): """ Instance attributes: alias_name : string only for alias types: the name of this type (the alias) alias_type : ValueType the aliased type (what 'alias_name' stands for) """ metatype = "alias" def __init__ (self, schema): ValueType.__init__(self, schema) self.alias_name = None self.alias_type = None def __str__ (self): return self.alias_name def set_alias_type (self, alias_name, alias_type): self.alias_name = alias_name self.alias_type = alias_type def get_alias_type (self): return self.alias_type def simple_check (self, value): return self.alias_type.simple_check(value) def check_value (self, value, context): self.count_object(context) return self.alias_type.check_value(value, context) def count_object (self, context): context.count_object(self, self.alias_name) def uncount_object (self, context): context.uncount_object(self, self.alias_name) # -- Utility functions ------------------------------------------------- def describe_value (value): if is_instance_object(value): # It's an instance of either a vanilla Python class or an # ExtensionClass. return "instance of %s" % get_full_classname(value.__class__) elif value is None: return "None" else: if (type(value) in (types.ListType, types.TupleType, types.DictionaryType) and len(value) > 10): str_value = "... elided ..." elif type(value) is types.StringType and len(value) > 100: str_value = value[0:20] + " ... " + value[-20:] else: str_value = repr(value) return "%s (%s)" % (get_type_name(type(value)), str_value)