<filename>download/gnosis/xml/validity/_validity.py
"""Library of Python objects that conform to XML validity rules

Requires: Python 2.2+
(may use new-style classes, slots, properties, metaclasses)

TODO: Need ANY type for <!ELEMENT tag ANY> declarations
      (not to be confused with Any(Quantification)
"""
from operator import add
from sys import maxint

class ValidityError(TypeError): pass
class LengthError(ValidityError): pass
class ReprError(ValidityError): pass

def match_type(item, types):
    # We might use metaclass, so don't check actual 'type(item)'
    # ...instead, just be satisfied if it -looks- like right thing
    return repr(type(item)) in map(repr, types)

class PCDATA(unicode):
    def __init__(self, s):
        self._tag = self.__class__.__name__
    def __str__(self):
        if self._tag[0] == '_' or self.__class__ is PCDATA:
            return self.encode('utf-8')
        else:
            return '<%s>%s</%s>\n' % \
                   (self._tag, self.encode('utf-8'), self._tag)
    def validate(self): pass

toPCDATA = lambda lst: map(PCDATA, lst)

class EMPTY(object):
    def __init__(self, initlist=[]):
        self._tag = self.__class__.__name__
    def __str__(self):
        self.validate()
        return '<%s />' % (self._tag)
    def validate(self):
        if self._tag[0] == '_':
            raise ReprError, \
                  "Initial underscore in class %s indicates non-tag" \
                  % (self._tag,)

class Or(object):
    def __init__(self, disjunct):
        if not hasattr(self.__class__, '_disjoins'):
            raise NotImplementedError, \
                  "Child of Abstract Class Or must specify list of disjoins"
        self._tag = self.__class__.__name__
        disjunct = self.lift(disjunct)
        self.checkitem(disjunct)
        self._data = disjunct
    def __str__(self):
        if self._tag[0] == '_':
            return '%s' % self._data
        else:
            return '<%s>%s</%s>\n' % (self._tag, self._data, self._tag)
    def checkitem(self, item):
        if not match_type(item, self._disjoins):
            raise ValidityError, \
                  "\n%s not in: \n  %s" % \
                  (type(item),'\n  '.join(map(repr,self._disjoins)))
    def validate(self):
        self.checkitem(self._data)
    def lift(self, item):
        if PCDATA in self._disjoins and type(item) in (str, unicode):
            item = PCDATA(item)
        # XXX -- can we reason abt other disjoins and their initializers?
        return item


class Seq(tuple):
    """Abstract class for sequence of elements

    Inheritance from immutable type is tricky.  A mutable class like
    Quantification descendents can 'lift' initialization arguments before
    calling base type initialization.  Seq children cannot do this.
    Therefore, use of the factory-function LiftSeq is generally recommended
    for more flexible initialization of a Seq object, e.g.:

        class tag1(Some): _type = whatever
        elm1 = tag1([whatever('this'),  whateverlike])
        class tag2(Seq): _order = (this, that, other)
        elm2 = LiftSeq(tag1, (thislike, thatlike, otherlike))

    The actual list parameter to tag1() can contain 'liftable' items,
    but the same effect for Seq passes 'tag2' to LiftSeq().
    """
    def __init__(self, inittup):
        if not hasattr(self.__class__, '_order'):
            raise NotImplementedError, \
                  "Child of Abstract Class Seq must specify order"
        if not isinstance(self._order, tuple):
            raise ValidityError, "Seq must have tuple as 'order'"
        self.validate()
        self._tag = self.__class__.__name__
    def __str__(self):
        contents = ''.join([str(item) for item in self])
        if self._tag[0] == '_':
            return contents
        else:
            return '<%s>%s</%s>\n' % (self._tag, contents, self._tag)
    def validate(self):
        for item, type_ in zip(self, self._order):
            if not match_type(item,[type_]):
                raise ValidityError, \
                      "Item %s must be of type %s (in %s)" % \
                      (type(item), type_, self._tag)

def LiftSeq(klass, items):
    #-- Make sure klass has necessary _order specification
    if not hasattr(klass, '_order'):
        raise NotImplementedError, \
              "Child of Abstract Class Seq must specify order"
    #-- Examine items:
    # (1) is it the right length?
    if len(items) != len(klass._order):
        raise LengthError, \
              "Sequence for %s must contain %s elements" % \
              (klass.__name__, len(klass._order))
    # (2) is each item usable (lifted if necessary)
    lifted = []
    for item, type_ in zip(items, klass._order):
        if match_type(item,[type_]):
            lifted.append(item)
        elif issubclass(type_, Seq):
            lifted.append(LiftSeq(type_, item))
        else:
            lifted.append(type_(item))
    return klass(tuple(lifted))

class Quantification(list):
    def __init__(self, initlist=[]):
        if not hasattr(self.__class__, '_type'):
            raise NotImplementedError, \
                  "Child of Abstract Quantification must specify type_"
        self._tag = self.__class__.__name__
        initlist = self.lift(initlist)
        if not self.min_length <= len(initlist) <= self.max_length:
            raise LengthError, self.length_message % self._tag
        else:
            list.__init__(self, initlist)
    def __setitem__(self, pos, item):
        self.checkitem(item)
        list.__setitem__(self, pos, item)
    def append(self, item):
        self.extend([item])
    def extend(self, items):
        items = self.lift(items)
        if len(self)+len(items) > self.max_length:
            raise LengthError, self.length_message % self._tag
        else:
            for item in items:
                self.checkitem(item)
            list.extend(self, items)
    def __iadd__(self, items):
        self.extend(items)
        return self
    def __delitem__(self, i):
        if len(self)-1 < self.min_length:
            raise LengthError, self.length_message % self._tag
        list.__delitem__(self, i)
    def __str__(self):
        contents = ''.join([str(item) for item in self])
        if self._tag[0] == '_':
            return contents
        else:
            return '<%s>%s</%s>\n' % (self._tag, contents, self._tag)
    def validate(self, deep=1):
        if not self.min_length <= len(self) <= self.max_length:
            raise LengthError, self.length_message % self._tag
        if deep:
            for item in self: item.validate()
    def checkitem(self, item):
        if not match_type(item,[self._type]):
            raise ValidityError, \
                  "Items in %s must be of type %s (not %s)" % \
                  (self._tag, self._type, type(item))
    def lift(self, items):
        # To be friendly, a list of items can be lifted into a list
        # or the type itself.  Extra nice is that we also "lift" a
        # single item into a list of items
        if type(items) not in (tuple, list):
            items = [items]
        lifted = []
        for item in items:
            if match_type(item,[self._type]):
                lifted.append(item)
            elif Seq in self._type.__bases__:
                lifted.append(LiftSeq(self._type, items))
            else:
                lifted.append(self._type(item))
        return lifted

class Some(Quantification):
    length_message = "List <%s> must have length >= 1"
    min_length = 1
    max_length = maxint

class Maybe(Quantification):
    length_message = "List <%s> must have length zero or one"
    min_length = 0
    max_length = 1

class Any(Quantification):
    length_message = "List <%s> may have any length"
    min_length = 0
    max_length = maxint

