"""$URL: svn://svn/repos/trunk/dulcinea/lib/specified.py $
$Id: specified.py 21319 2003-04-09 20:17:45Z dbinger $

This module provides
  Specification
    instances hold type specifications and documentation strings.
  Specified
    subclasses can set class attributes to Specification instances, and
    then the methods inherited from Specified are useful for
    validating, setting, and documenting properties of instances.
    The attributes of instances of Specified can be accessed directly
    in the normal manner.


def example_usage ():
    class A(Specified):
        color = spec(any(str, int), 'Color name or number')
        taste = nspec(any('sweet', 'sour'))
    a = A()
    a.set(color='blue')
    assert a.color == 'blue'
    a.set(color=1, taste='sweet')
    try:
        a.set(color=2.3)
    except SpecError:
        pass
    assert a.color == 1
    assert [] == a.get_specification_problems()
    a.mistake = 42
    assert len(a.get_specification_problems()) == 1  # non-Specified attribute
    a.color = (2,3,4) # Direct attribute access works as usual.
    assert len(a.get_specification_problems()) == 2 # color is bad
    assert a.valid(color='red')
    class B(A):
        color = nspec(float)
        smell = nspec(any(None, 'bad'))
    b = B()
    b.set(smell=None, color=3.4, taste='sour')
    print b.get_specification_doc()
    assert 'sweet' in b.get_spec('taste').get_args()


"""

from dulcinea.spec import *

class Specification:
    """
    Instance attributes:
      doc : string
      spec : any
    """

    def __init__ (self, spec, doc=''):
        self.doc = doc
        self.spec = spec

    def format (self):
        """-> str
        Return a formatted string describing this spec and corresponding doc.
        """
        doc = '\n  '.join([line.strip()
                           for line in self.doc.split('\n')])
        if doc:
            return self.format_spec() + '\n  ' + doc
        else:
            return self.format_spec()

    def valid (self, value):
        """(value : any) -> bool
        Does the value match this spec?
        """
        return match(value, self.spec)

    def format_spec (self):
        """() -> str
        Return a string version of the spec.
        """
        return format_spec(self.spec)

    def get_spec (self):
        """() -> anything
        Return the spec.
        """
        return self.spec

def get_specifications (klass):
    """(klass:Specification) -> { str : Specification }
    Returns a dictionary mapping names to Specification instances
    that would apply to instances of klass.
    """
    specifications = {}

    def deepdir (klass):
        """
        This function is here, as a hack replacement for dir, because
        the dir of an ExtensionClass does not include inherited
        attributes.  When we no longer need ExtensionClasses, this
        should be removed and replaced by plain dir.
        """
        bases = list(klass.__bases__)
        bases.reverse()
        result = {}
        for base in bases:
            result.update(deepdir(base))
        for name in dir(klass):
            result[name]=1
        return result

    for name in deepdir(klass):
        try:
            value = getattr(klass, name)
        except AttributeError:
            continue
        if isinstance(value, Specification):
            specifications[name] = value
    return specifications

def get_specification_doc (klass):
    """(klass:Specified) -> str
    Return a string containing the docs of all specifications of klass.
    """
    specification_items = get_specifications(klass).items()
    specification_items.sort()
    return '\n'.join([ "%s : %s" % (name, specification.format())
                       for name, specification in
                       specification_items])

def update_docstring (specified):
    """(specified:Specified) -> str
    Insert the generated docs for the specified attributes
    into the docstring of the specified class.  Return
    the new specified.__doc__.
    """
    specified_header = 'Specified instance attributes:\n'
    specified_doc = (specified_header +
                     (get_specification_doc(specified) or 'none'))
    specified_doc = specified_doc.replace('\n', '\n  ')
    if not specified.__doc__:
        specified.__doc__ = specified_doc
    else:
        header_pos = specified.__doc__.find(specified_header)
        if header_pos == -1:
            specified.__doc__ += '\n\n' + specified_doc
        else:
            specified.__doc__ = (specified.__doc__[:header_pos] +
                                 specified_doc)
    return specified.__doc__


class Specified:

    """
    Classes with Specifications as class attributes get some nice methods
    for using them by inheriting from this class.

    Instance attributes: none
    """

    def get_specifications (self):
        """() -> { str : Specification }
        Returns a dictionary mapping names to Specification instances
        that would apply to instances of this class.
        """
        return get_specifications(self.__class__)

    def get_specification_doc (self):
        """
        Return a nicely formated string that documents the
        specifications of this class.
        """
        return get_specification_doc(self.__class__)

    def get_specification_problems (self):
        """() -> [str]
        Return a list of reasons why the attributes of this
        instance do not match the specifications.
        """
        reasons = []
        for name, specification in self.get_specifications().items():
            value = getattr(self, name)
            if not specification.valid(value):
                reasons.append(
                    ('%r.%s:\n'
                     '  Expected: %s\n'
                     '  Got: %r\n') % (
                    self, name, specification.format_spec(), value))
        for name, value in vars(self).items():
            if not isinstance(getattr(self.__class__, name, None),
                              Specification):
                reasons.append('Found non-specified attribute: %r.%s (=%r)' %
                               (self, name, value))
        return reasons

    def set_specified_attribute (self, name, value):
        """(name:str, value:any)
        Set the value of the named specification.
        Raises a SpecError if a value is not a valid spec for
        the specification.
        """
        specification = getattr(self.__class__, name)
        if not specification.valid(value):
            raise SpecError(
                ('\n  %s.%s:\n'
                 '    Expected: %s\n'
                 '    Got: %r\n') % (self,
                                     name,
                                     specification.format_spec(),
                                     value))
        setattr(self, name, value)

    def set (self, **keywords):
        """
        Set the instance attributes as specified in keyword arguments.
        Raise a SpecError if a value is not a valid for
        the corresponding specification.
        """
        for name, value in keywords.items():
            self.set_specified_attribute(name, value)

    def valid_specification (self, name, value):
        """(name:str, value:any) -> boolean
        Check the validity of the specification value.
        """
        return getattr(self.__class__, name).valid(value)

    def valid (self, **keywords):
        """(**keywords) -> boolean
        Check the validity of the values.
        If all values are valid for the corresponding specifications,
        return True.
        """
        for name, value in keywords.items():
            specification = getattr(self.__class__, name)
            if not specification.valid(value):
                return False
        return True

    def get_spec (self, name):
        """(str) -> anything
        """
        return getattr(self.__class__, name).get_spec()

def spec (spec, *docs):
    """(any, *str) -> Specification
    An alternative Specification constructor.
    """
    doc = '\n'.join(docs)
    return Specification(spec, doc)

def nspec (spec, *docs):
    """(any, *str) -> Specification
    Like spec, but this uses a modified Specification (if needed) to make
    sure that None is a valid value.
    For applications that always want None as a valid value, this
    allows us to avoid using the any(None,*) pattern around every spec.
    """
    doc = '\n'.join(docs)
    s = Specification(spec, doc)
    if s.valid(None):
        return s
    if isinstance(spec, any):
        return Specification(any(None, *spec.get_args()), doc)
    return Specification(any(None, spec), doc)
