"""grouch.schema

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.
"""

# created 2000/12/13 (from mems.tools.schema module)
#
# Copyright (c) 2001 CNRI.  All Rights Reserved.

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

import string
from copy import copy
import types
from grouch.util import \
     get_full_classname, get_class_object, is_class_object, is_instance_object


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 = { '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.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', 'atomic_type_names', and
        'atomic_types' dictionaries from 'atomic_type_values'.
        """
        self.atomic_type_objects = {}
        self.atomic_type_names = {}
        self.atomic_types = {}
        for (name, value) in self.atomic_type_values.items():
            tipe = type(value)
            self.atomic_type_objects[name] = tipe
            self.atomic_type_names[tipe] = name
            self.atomic_types[name] = self.make_atomic_type(name)


    # -- Pickle support ------------------------------------------------
    # needed because type objects can't be pickled, so we must eradicate
    # them from the object at pickle-time (__getstate__) and resuscitate
    # them at unpickle-time (__setstate__)

    def __getstate__ (self):
        dict = copy(self.__dict__)
        del dict['atomic_type_objects'], dict['atomic_type_names']
        dict['scanner'] = dict['parser'] = None
        return dict

    def __setstate__ (self, dict):
        self.__dict__.update(dict)
        self._create_type_dicts()
        memo = {}                       # what have we visited?
        for klassdef in self.klass_definitions.values():
            klassdef._fixup_atomic_types(memo)
        for tipe in (self.atomic_types.values() + self.alias_value.values()):
            tipe._fixup_atomic_types(memo)

    # -- 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


    # -- 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,
        "type(value).__name__" is used.
        """
        tipe = type(value)
        if name is None:
            name = tipe.__name__
        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 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 not isinstance(schema, 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 _fixup_atomic_types (self, memo):
        if memo.get(id(self)): return
        memo[id(self)] = 1
        for subtype in self.attr_types.values():
            subtype._fixup_atomic_types(memo)

    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)
        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
