"""$URL: svn://svn/repos/trunk/dulcinea/lib/spec.py $
$Id: spec.py 21308 2003-04-09 17:56:46Z dbinger $

This module provides tools for matching values to specs.

In particular, it provides the functions
  match(),
  require()

This module also provides tools for constructing specs.

  sequence
    this class makes it convenient to specify a non-standard
    list-like container and its elements.
  mapping
    this class makes it convenient to specify a non-standard
    dict-like container and its items.
  any, no, all, eq, equal
    these classes can build more complex specifications.
  length
    a class for specifying length requirements
  string
    a class for specifying strings with length requirements
  boolean
    this function makes it convenient to specify booleans.
  anything
    this function works as the universal spec.

"""

from types import *


class SpecError (TypeError, ValueError):

    "Raise this so that the user may catch problems as either type."""


def require (value, spec, message=None):
    if not match(value, spec):
        error = ('\n  Expected: %s\n'
                 '  Got: %r\n') % (format_spec(spec), value)
        if message:
            error = '(%s)%s' % (message, error)
        raise SpecError(error)


def match (value, spec):
    """
    Return True or False depending on whether or not value matches
    the given type specification.  Here are the available type
    specifications:

    Type Spec                Matches

    a type or class         any instance of the type

    a callable              anything for which <spec>(value) is true,
                            so you can do arbitrary checking.

    a list (or tuple)       any list (tuple) whose elements all match the
    with length 1           enclosed type specification.

    a list (or tuple)       any list (tuple) whose sequence of elements match
    with length > 1         the sequence of corresponding type specifications.

    a dict                  any dict for which every item matches some item in
                            this dictionary.

    other                   any value for which value == other.

    """
    if isinstance(spec, (FunctionType, SpecOperator)):
        return spec(value)
    spec_type = type(spec)
    if spec_type in (ListType, TupleType):
        if type(value) is not spec_type:
            return False
        if len(spec) == 1:
            element_type = spec[0]
            for element in value:
                if not match(element, element_type):
                    return False
            return True
        else:
            if len(value) != len(spec):
                return False
            for element, element_type in zip(value, spec):
                if not match(element, element_type):
                    return False
            return True
    if spec_type is DictType:
        if type(value) is not DictType:
            return False
        for key, val in value.items():
            for key_type, value_type in spec.items():
                if (match(key, key_type) and
                    match(val, value_type)):
                    break
            else:
                return False
        return True
    if spec_type is type(value):
        return value == spec
    try:
        return isinstance(value, spec)
    except TypeError:
        pass
    if callable(spec):
        return spec(value)
    return False


def format_spec (spec):
    """
    Returns the canonical string representation of the spec.
    """
    if spec is None:
        return 'None'
    if type(spec) is TupleType:
        return '(%s)' % ', '.join(map(format_spec, spec))
    if type(spec) is ListType:
        return '[%s]' % ', '.join(map(format_spec, spec))
    if type(spec) is DictType:
        return ('{%s}' %
                ', '.join(["%s : %s" % (format_spec(key_type),
                                        format_spec(val_type))
                           for key_type, val_type in spec.items()]))
    if hasattr(spec, '__name__'):
        return spec.__name__
    if type(spec) is StringType:
        return repr(spec)
    return str(spec)


def anything (value):
    """
    The universal spec.
    """
    return True


class SpecOperator:
    """
    The superclass of all of the spec operators.
    Constructors of subclasses should assign to self.args
    a tuple of the arguments to the constructor.
    """

    def get_args (self):
        return self.args

    def format_args (self):
        return ', '.join(map(format_spec, self.args))

    def __str__ (self):
        return "%s(%s)" % (self.__class__.__name__, self.format_args())


class sequence (SpecOperator):

    """
    Use this to specify a sequence.
    """

    def __init__ (self, element_spec=anything, container_spec=anything):
        self.container_spec = container_spec
        self.element_spec = element_spec
        self.args = (element_spec, container_spec)

    def __call__ (self, value):
        if not match(value, self.container_spec):
            return False
        element_spec = self.element_spec
        for element in value:
            if not match(element, element_spec):
                return False
        return True


class mapping (SpecOperator):

    """
    Use this to specify a mapping.
    """

    def __init__ (self, dict_spec=anything, container_spec=anything):
        self.dict_spec = dict_spec
        self.container_spec = container_spec
        self.args =  (self.dict_spec, container_spec)

    def __call__ (self, value):
        if not match(value, self.container_spec):
            return False
        try:
            dict_value = dict(value.items())
        except AttributeError:
            return False
        if not match(dict_value, self.dict_spec):
            return False
        return True


class instance (SpecOperator):

    """
    Use this to specify an instance of a class with a given name
    when the class itself is not available.
    """

    def __init__ (self, klass_name):
        self.klass_name = klass_name
        self.args = (klass_name,)

    def __call__ (self, value):
        if hasattr(value, '__class__'):
            classes = [value.__class__]
            while classes:
                if classes[0].__name__ == self.klass_name:
                    return True
                classes = classes[1:] + list(classes[0].__bases__)
        return False


class ConnectiveSpecOperator (SpecOperator):

    def __init__ (self, *specs):
        self.specs = specs
        self.args = self.specs


class no (ConnectiveSpecOperator):

    """
    Use this to specify a negation of specs.
    Example: no(None)
    """

    def __call__ (self, value):
        for spec in self.specs:
            if match(value, spec):
                return False
        return True


class all (ConnectiveSpecOperator):

    """
    Use this to specify a conjunction of specs.
    Example:
      all([int], length(3)) specifies values that are lists of 3 ints.
    """

    def __call__ (self, value):
        for spec in self.specs:
            if not match(value, spec):
                return False
        return True


class any (ConnectiveSpecOperator):

    """
    Use this to specify a disjunction of specs.
    Examples:
      any(int, str) specifies values that are ints or strs.
    """

    def __call__ (self, value):
        for spec in self.specs:
            if match(value, spec):
                return True
        return False


class interval (SpecOperator):

    def __init__ (self, min=anything, max=anything):
        self.min = min
        self.max = max

    def format_args(self):
        if self.max is anything:
            if self.min is anything:
                return ''
            return format_spec(self.min)
        else:
            return '%s, %s' % (format_spec(self.min), format_spec(self.max))

    def __call__ (self, value):
        if self.min not  in (None, anything):
            if self.max is anything:
                return value >= self.min
            elif value < self.min:
                return False
        if self.max not  in (None, anything):
            if self.min is anything:
                return value <= self.max
            elif value > self.max:
                return False
        return True


class length (interval):

    """
    Use this to specify specs by ranges of length.
    Examples:
      length(3, None) specifies values of length 3 or more.
      length(3) specifies values of length 3.
    """

    def __call__ (self, value):
        return interval.__call__(self, len(value))


class string (length):

    """
    Use this to specify strings with length restrictions.
    Examples:
      string(3, None) specifies strings of length 3 or more.
      string(3) specifies strings of length 3.
    """

    def __call__ (self, value):
        return (isinstance(value, StringType) and
                interval.__call__(self, len(value)))


class eq (SpecOperator):

    """
    Use this to specify specs exact value.
    Examples:
      eq(SpecOperator) matches the SpecOperator class, but nothing else.

    """

    def __init__ (self, value):
        self.value = value

    def format_args(self):
        return repr(self.value)

    def __call__ (self, value):
        return value is self.value


class equal (SpecOperator):

    """
    Use this to specify specs exact value.
    Examples:
      eq([2,3]) matches any list [2,3]

    """

    def __init__ (self, value):
        self.value = value

    def format_args(self):
        return repr(self.value)

    def __call__ (self, value):
        return value == self.value


def boolean (value):
    """
    Use this to specify the normal boolean.
    Note: the builtin 'bool' also works as a specification,
    but it stands for the set of all values that are logically true,
    a totally different set from this one.
    """
    return value in (True, False)


