"""$URL: svn+ssh://svn/repos/trunk/grouch/lib/schema.py $ $Id: schema.py 24752 2004-07-21 15:42:43Z dbinger $ The main classes that represent an object schema: - ObjectSchema all type information needed to represent a collection of related classes: atomic types, type aliases, and class definitions - ClassDefinition an ordered collection of attribute definitions (names and types) Attribute types are represented by the ValueType class, in grouch.valuetype. """ import string from copy import copy import types from grouch.util import \ get_full_classname, \ is_class_object, is_instance_object, \ get_type_name class InvalidAlias: """ Indicates that an alias name cannot be used due to collisions. """ def __nonzero__(self): return 0 class ObjectSchema: """ Holds all the type information needed to represent a collection of related classes or to typecheck a graph of related objects. This includes atomic types, type aliases, and class definitions. Instance attributes: atomic_type_values : { string : any } maps names of atomic types to sample values of those types -- this seems like a weird way to store type information, but we need to keep sample values around because type objects cannot be pickled. atomic_type_objects : { string : type } maps atomic type names to their corresponding type objects -- specifies which names can be parsed as atomic types in this type system (derived from 'atomic_type_values') atomic_type_names : { type : string } inverse of 'atomic_type_objects' (also derived from 'atomic_type_values') atomic_types : { string : AtomicType } maps atomic type names to the ValueType instance that represents that type (and, more importantly, can typecheck values of that type) aliases : [string] list of type aliases (we want to write out aliases in the order they were defined, so order matters) alias_value : { name:string : type:ValueType } dictionary of type alias expansions -- each alias expands to a ValueType object, so these are "semantic" rather than textual aliases klasses : [string] the list of classes in the schema, as fully-qualified names (eg. "package.module.Class") klass_definitions : { string : ClassDefinition } maps fully-qualified class names to their class definitions scanner : TypeScanner parser : TypeParser """ # Standard atomic types -- just use ones that have an obvious # sample value (this has the benefit of leaving off mildly # exotic types like functions, classes, files, etc.). ATOMIC_TYPE_VALUES = { 'bool': False, 'int': 0, 'long': 0L, 'float': 0.0, 'complex': 0j, 'string': "", } def __init__ (self): self.atomic_type_values = copy(self.ATOMIC_TYPE_VALUES) self._create_type_dicts() # sets 'atomic_type_objects' and # 'atomic_type_names' attributes self.atomic_types = {} for (name, value) in self.atomic_type_values.items(): self.atomic_types[name] = self.make_atomic_type(name) self.aliases = [] self.alias_value = {} self.klasses = [] self.klass_definitions = {} # These two are created on-demand, out of superstition that # creating a parser might be expensive. self.scanner = None self.parser = None def __repr__ (self): return "<%s at %x>" % (self.__class__.__name__, id(self)) def _create_type_dicts (self): """ Derive the 'atomic_type_objects' and 'atomic_type_names', dictionaries from 'atomic_type_values'. """ self.atomic_type_objects = {} self.atomic_type_names = {} for (name, value) in self.atomic_type_values.items(): tipe = type(value) self.atomic_type_objects[name] = tipe self.atomic_type_names[tipe] = name # -- Pickle support ------------------------------------------------ # needed because type objects can't be pickled, so we must eradicate # them from the object at pickle-time (__getstate__) and revive # them at unpickle-time (__setstate__) def __getstate__ (self): dict = copy(self.__dict__) dict['atomic_type_objects'] = None dict['atomic_type_names'] = None dict['scanner'] = dict['parser'] = None return dict def __setstate__ (self, dict): self.__dict__.update(dict) self._create_type_dicts() # -- Type lookup methods ------------------------------------------- def get_type (self, typeinfo): """get_type(typeinfo : type|string|class|ValueType) -> ValueType Helps ensure that you have a ValueType object in hand, no matter what type information you have to begin with. Behaves differently depending on the 'typeinfo' value you pass in: type object: returns an AtomicType instance if this type object is recognized by the schema as an atomic type; otherwise raises ValueError string: if 'typeinfo' is the name of an atomic type known to the schema, returns an AtomicType instance. If it is the (dotted) name of a class known to the schema, returns an InstanceType instance. If it is an alias known to the schema, returns an AliasType instance. Otherwise raises ValueError. class object: returns an InstanceType instance if this class is known to the schema; otherwise raises ValueError ValueType instance: returns 'typeinfo' as-is Raises TypeError if 'typeinfo' is anything else. """ from grouch.valuetype import ValueType, AliasType, BooleanType, AnyType typetype = type(typeinfo) if typetype is types.TypeType: try: name = self.atomic_type_names[typeinfo] return self.atomic_types[name] except KeyError: raise ValueError, "not an atomic type object: %s" % `typeinfo` elif typetype is types.StringType: if typeinfo == "boolean": return BooleanType(self) elif typeinfo == "any": return AnyType(self) elif self.atomic_types.has_key(typeinfo): return self.atomic_types[typeinfo] elif self.alias_value.has_key(typeinfo): atype = AliasType(self) atype.set_alias_type(typeinfo, self.alias_value[typeinfo]) return atype elif self.klass_definitions.has_key(typeinfo): return self.make_instance_type(typeinfo) else: raise ValueError, \ ("invalid type %s: not an atomic type, alias, " "or known class" % `typeinfo`) elif is_class_object(typeinfo): return self.make_instance_type(typeinfo) elif isinstance(typeinfo, ValueType): return typeinfo else: raise TypeError, \ ("invalid 'typeinfo': must be a type object, class object, " "string, or ValueType (not %s)" % `typeinfo`) # get_type () # -- Type creation methods ----------------------------------------- def parse_type (self, typestr): if self.scanner is None or self.parser is None: from grouch.type_parser import TypeScanner, TypeParser self.scanner = TypeScanner() self.parser = TypeParser(schema=self) tokens = self.scanner.tokenize(typestr) return self.parser.parse(tokens) def make_atomic_type (self, tipe): from grouch.valuetype import AtomicType t = AtomicType(self) t.set_type(tipe) return t def make_instance_type (self, klass): """make_instance_type(klass : string|class) -> InstanceType Create a new InstanceType to describe values that should be instances of 'klass'. 'klass' must be either a fully-qualified class name or class object. The class definition (needed to create the InstanceType object) will be looked up in the schema. Raises ValueError if 'klass' is unknown to this schema. """ from grouch.valuetype import InstanceType if type(klass) is types.StringType: klass_name = klass else: # class object klass_name = get_full_classname(klass) if not self.get_class_definition(klass_name): raise ValueError, "class not in schema: %s" % klass_name t = InstanceType(self) t.set_class_name(klass_name) return t def make_list_type (self, element_type): from grouch.valuetype import ListType t = ListType(self) t.set_element_type(element_type) return t def make_tuple_type (self, element_types, extended=0): from grouch.valuetype import TupleType t = TupleType(self) t.set_element_types(element_types) t.set_extended(extended) return t def make_dictionary_type (self, key_type, value_type): from grouch.valuetype import DictionaryType t = DictionaryType(self) t.set_item_types(key_type, value_type) return t def make_set_type (self, element_type): from grouch.valuetype import SetType t = SetType(self) t.set_element_type(element_type) return t def make_instance_container_type (self, name, element_type): name = str(name) # Check if this is really an alias for something (presumably a class!) alias_val = self.get_alias(name) if alias_val: if not alias_val.is_plain_instance_type(): raise ValueError, \ ("%s is an alias, but not for a plain instance type" % name) klass_name = alias_val.klass_name else: klass_name = name from grouch.valuetype import InstanceContainerType ictype = InstanceContainerType(self) ictype.set_class_name(klass_name) ictype.set_container_type(element_type) return ictype def make_union_type(self, typelist): from grouch.valuetype import UnionType utype = UnionType(self) utype.set_union_types(typelist) return utype # -- Atomic type methods ------------------------------------------- def add_atomic_type (self, value, name=None): """add_atomic_type(value : any, name : string = None) Add a new atomic type to the schema. 'value' is a sample value of the type, *not* a type object. (We need a sample value because type objects cannot be pickled.) 'name' is the name to assign to the type of 'value'; if not supplied, the type's name (from grouch.util.get_type_name()) is used. """ tipe = type(value) if name is None: name = get_type_name(tipe) if (self.atomic_type_objects.has_key(name) or self.atomic_type_names.has_key(tipe)): raise ValueError, "type %s already known" % tipe self.atomic_type_values[name] = value self.atomic_type_objects[name] = tipe self.atomic_type_names[tipe] = name self.atomic_types[name] = self.make_atomic_type(tipe) def is_atomic_type_name (self, name): return self.atomic_type_objects.has_key(name) def get_atomic_type_name (self, type_obj): """get_atomic_type_name(type_obj : type) -> string Return the type name corresponding to the type object 'type_obj'. Raises ValueError if 'type_obj' is not an atomic type in this schema. (Eg. IntType and FloatType are standard atomic types, and this method will return "int" and "float" for them. FunctionType is not a standard atomic type, though, so unless you add it to the list of atomic types in your object schema, passing it here will raise ValueError.) """ try: return self.atomic_type_names[type_obj] except KeyError: raise ValueError, "not an atomic type object: %s" % `type_obj` def get_atomic_type_object (self, tipe): """get_atomic_type_object(tipe : type|string) -> type Ensures that you have the type object for one of this schema's atomic types in hand. If 'tipe' is a type object, checks that it is an atomic type in this schema and returns 'tipe' back to you if so. If 'tipe' is a string, checks that it names an atomic type in this schema and returns the corresponding type object if so. If 'tipe' does not correspond to an atomic type in this schema, raise ValueError. """ # Ugh, mildly nasty hack that lets us make extension classes # atomic types. try: from ExtensionClass import ExtensionClass as EC except ImportError: EC = None if (type(tipe) is types.TypeType) or (EC and type(tipe) is EC): if not self.atomic_type_names.has_key(tipe): raise ValueError, "not an atomic type: %s" % `tipe` return tipe else: try: return self.atomic_type_objects[tipe] except KeyError: raise ValueError, "not an atomic type name: %s" % `tipe` # -- Type alias methods -------------------------------------------- def has_alias (self, alias): return self.alias_value.has_key(alias) def get_alias (self, alias): return self.alias_value.get(alias) def invalidate_alias (self, alias): self.aliases.remove(alias) self.alias_value[alias] = InvalidAlias() def add_alias (self, alias_name, alias_value): from grouch.valuetype import ValueType if self.alias_value.has_key(alias_name): raise ValueError, "alias '%s' already exists" % alias_name if type(alias_value) is types.StringType: alias_value = self.parse_type(alias_value) elif not isinstance(alias_value, ValueType): raise TypeError, "'alias_value' must be a string or ValueType" self.aliases.append(alias_name) self.alias_value[alias_name] = alias_value # -- Class definition methods -------------------------------------- def add_class (self, classdef): name = classdef.name if self.klass_definitions.has_key(name): raise RuntimeError, "class '%s' already defined" % name self.klasses.append(name) self.klass_definitions[name] = classdef def get_class_definition (self, klass): """get_class_definition(klass : string|class) -> ClassDefinition""" if type(klass) is types.StringType: return self.klass_definitions.get(klass) elif is_class_object(klass): klass_name = get_full_classname(klass) return self.klass_definitions.get(klass_name) def get_class_names (self): """Return the list of all class names in the schema.""" return self.klasses # -- Type-checking methods ----------------------------------------- def check_value (self, value, context): if not is_instance_object(value): raise TypeError, "not a class instance: %s" % `value` inst_type = self.make_instance_type(value.__class__) return inst_type.check_value(value, context) # -- Output methods ------------------------------------------------ def write_aliases (self, file): for name in self.aliases: file.write("alias %s = %s\n" % (name, self.alias_value[name])) # class ObjectSchema class ClassDefinition: """ Represents the definition of one class in an object schema. This includes the list of attributes and the type of each attribute. Instance attributes: name : string the fully-qualified name of this class (eg. "pkg.module.Class") schema : ObjectSchema the object schema to which this class definition belongs bases : [string] list of this class' base classes (as fully-qualified class names) attrs : [string] list of instance attributes defined by this class only attr_types : { string : ValueType } the type of each attribute in 'attrs' all_attrs : [string] list of all instance attributes expected (includes attributes from all superclasses) all_attr_types : { string : ValueType } the type of each attribute in 'all_attrs' """ # -- Initialization/construction ----------------------------------- def __init__ (self, name, schema, bases=None): if not type(name) is types.StringType: raise TypeError, "'name' not a string: %s" % `name` if schema.__class__.__name__ != 'ObjectSchema': raise TypeError, \ "'schema' not an ObjectSchema instance: %s" % `schema` self.name = name self.schema = schema self.attrs = [] # just from this class self.attr_types = {} self.all_attrs = [] # from this class + all bases classes self.all_attr_types = {} self.set_bases(bases) def __str__ (self): return self.name def __repr__ (self): return "<%s at %x: %s>" % (self.__class__.__name__, id(self), self) def set_bases (self, bases): if bases is None: self.bases = [] else: if not (type(bases) is types.ListType and map(type, bases) == [types.StringType] * len(bases)): raise TypeError, \ "'bases' not a list of strings: %s" % `bases` self.bases = bases def finish_definition (self): """Call this to signal that the definition of this class (and all its bases classes) is complete, ie. all attributes have been added and all bases classes are in the schema. Currently, just builds a list of all attributes from base classes for future reference. It's safe (in fact, it's essential) to call this again if you change the list of base classes, or change the attribute list for this class or any of its ancestors. """ self._find_all_attrs() # -- Private methods ----------------------------------------------- # (currently just for traversing the superclass tree to find # all attributes) def _find_bases (self): """find_bases() -> [ClassDefinition] Find all of this class' ancestor classes. Return a list of all ancestor class definitions, including the current class definition. Classes higher up the superclass tree come first; classes to the left in the superclass tree (ie. earlier in any class' __bases__ list) come earlier. (Thus 'self' is always the last element of the returned list.) """ ancestors = [] for base_classname in self.bases: base_classdef = self.schema.get_class_definition(base_classname) if not base_classdef: raise ValueError, \ ("%s: base class %s not in schema" % (self.name, base_classname)) ancestors.extend(base_classdef._find_bases()) ancestors.append(self) return ancestors def _find_all_attrs (self): bases = self._find_bases() for base in bases: self.all_attrs.extend(base.attrs) self.all_attr_types.update(base.attr_types) # XXX should we check that overridden attributes are # type-compatible? (essentially, do type-checking of the # object schema) # -- Attribute list manipulation ----------------------------------- def add_attribute (self, name, attr_type): from grouch.valuetype import ValueType if not isinstance(attr_type, ValueType): raise TypeError, \ "'attr_type' not a ValueType instance: %s" % `attr_type` if self.attr_types.has_key(name): raise RuntimeError, \ "class %s already has attribute '%s'" % (self.name, name) if name.startswith("__"): class_name = self.name.split(".")[-1] name = "_%s%s" % (class_name, name) self.attrs.append(name) self.attr_types[name] = attr_type def get_attribute_type (self, name): return self.all_attr_types.get(name) def num_attributes (self): return len(self.all_attrs) # -- Miscellaneous ------------------------------------------------- def write (self, file): if self.bases: bases = string.join(self.bases, ", ") file.write("class %s (%s):\n" % (self.name, bases)) else: file.write("class %s:\n" % self.name) if self.attrs: for attr in self.attrs: type = self.attr_types[attr] file.write(" %s : %s\n" % (attr, type)) else: file.write(" pass\n") # class ClassDefinition