thirdparty/google_appengine/lib/webob/webob/acceptparse.py
author Pawel Solyga <Pawel.Solyga@gmail.com>
Thu, 22 Jan 2009 22:04:00 +0000
changeset 908 8f7e89cacd2e
parent 109 620f9b141567
permissions -rwxr-xr-x
Remove unused decorators import from soc.views.models.role module. Patch by: Pawel Solyga Review by: to-be-reviewed

"""
Parses a variety of ``Accept-*`` headers.

These headers generally take the form of::

    value1; q=0.5, value2; q=0

Where the ``q`` parameter is optional.  In theory other parameters
exists, but this ignores them.
"""

import re

part_re = re.compile(
    r',\s*([^\s;,\n]+)(?:[^,]*?;\s*q=([0-9.]*))?')

def parse_accept(value):
    """
    Parses an ``Accept-*`` style header.

    A list of ``[(value, quality), ...]`` is returned.  ``quality``
    will be 1 if it was not given.
    """
    result = []
    for match in part_re.finditer(','+value):
        name = match.group(1)
        if name == 'q':
            continue
        quality = match.group(2) or ''
        if not quality:
            quality = 1
        else:
            try:
                quality = max(min(float(quality), 1), 0)
            except ValueError:
                quality = 1
        result.append((name, quality))
    return result

class Accept(object):
    """
    Represents a generic ``Accept-*`` style header.

    This object should not be modified.  To add items you can use
    ``accept_obj + 'accept_thing'`` to get a new object
    """

    def __init__(self, header_name, header_value):
        self.header_name = header_name
        self.header_value = header_value
        self._parsed = parse_accept(header_value)

    def __repr__(self):
        return '<%s at %x %s: %s>' % (
            self.__class__.__name__,
            abs(id(self)),
            self.header_name, str(self))

    def __str__(self):
        result = []
        for match, quality in self._parsed:
            if quality != 1:
                match = '%s;q=%0.1f' % (match, quality)
            result.append(match)
        return ', '.join(result)

    # FIXME: should subtraction be allowed?
    def __add__(self, other, reversed=False):
        if isinstance(other, Accept):
            other = other.header_value
        if hasattr(other, 'items'):
            other = sorted(other.items(), key=lambda item: -item[1])
        if isinstance(other, (list, tuple)):
            result = []
            for item in other:
                if isinstance(item, (list, tuple)):
                    name, quality = item
                    result.append('%s; q=%s' % (name, quality))
                else:
                    result.append(item)
            other = ', '.join(result)
        other = str(other)
        my_value = self.header_value
        if reversed:
            other, my_value = my_value, other
        if not other:
            new_value = my_value
        elif not my_value:
            new_value = other
        else:
            new_value = my_value + ', ' + other
        return self.__class__(self.header_name, new_value)

    def __radd__(self, other):
        return self.__add__(other, True)

    def __contains__(self, match):
        """
        Returns true if the given object is listed in the accepted
        types.
        """
        for item, quality in self._parsed:
            if self._match(item, match):
                return True

    def quality(self, match):
        """
        Return the quality of the given match.  Returns None if there
        is no match (not 0).
        """
        for item, quality in self._parsed:
            if self._match(item, match):
                return quality
        return None
    
    def first_match(self, matches):
        """
        Returns the first match in the sequences of matches that is
        allowed.  Ignores quality.  Returns the first item if nothing
        else matches; or if you include None at the end of the match
        list then that will be returned.
        """
        if not matches:
            raise ValueError(
                "You must pass in a non-empty list")
        for match in matches:
            for item, quality in self._parsed:
                if self._match(item, match):
                    return match
            if match is None:
                return None
        return matches[0]
    
    def best_match(self, matches, default_match=None):
        """
        Returns the best match in the sequence of matches.

        The sequence can be a simple sequence, or you can have
        ``(match, server_quality)`` items in the sequence.  If you
        have these tuples then the client quality is multiplied by the
        server_quality to get a total.

        default_match (default None) is returned if there is no intersection.
        """
        best_quality = -1
        best_match = default_match
        for match_item in matches:
            if isinstance(match_item, (tuple, list)):
                match, server_quality = match_item
            else:
                match = match_item
                server_quality = 1
            for item, quality in self._parsed:
                possible_quality = server_quality * quality
                if possible_quality < best_quality:
                    continue
                if self._match(item, match):
                    best_quality = possible_quality
                    best_match = match
        return best_match

    def best_matches(self, fallback=None):
        """
        Return all the matches in order of quality, with fallback (if
        given) at the end.
        """
        items = [
            i for i, q in sorted(self._parsed, key=lambda iq: -iq[1])]
        if fallback:
            for index, item in enumerate(items):
                if self._match(item, fallback):
                    items[index+1:] = []
                    break
            else:
                items.append(fallback)
        return items

    def _match(self, item, match):
        return item.lower() == match.lower() or item == '*'

class NilAccept(object):

    """
    Represents an Accept header with no value.
    """

    MasterClass = Accept

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

    def __repr__(self):
        return '<%s for %s: %s>' % (
            self.__class__.__name__, self.header_name, self.MasterClass)

    def __str__(self):
        return ''

    def __add__(self, item):
        if isinstance(item, self.MasterClass):
            return item
        else:
            return self.MasterClass(self.header_name, '') + item

    def __radd__(self, item):
        if isinstance(item, self.MasterClass):
            return item
        else:
            return item + self.MasterClass(self.header_name, '')

    def __contains__(self, item):
        return True

    def quality(self, match, default_quality=1):
        return 0

    def first_match(self, matches):
        return matches[0]

    def best_match(self, matches, default_match=None):
        best_quality = -1
        best_match = default_match
        for match_item in matches:
            if isinstance(match_item, (list, tuple)):
                match, quality = match_item
            else:
                match = match_item
                quality = 1
            if quality > best_quality:
                best_match = match
                best_quality = quality
        return best_match

    def best_matches(self, fallback=None):
        if fallback:
            return [fallback]
        else:
            return []

class NoAccept(NilAccept):

    def __contains__(self, item):
        return False

class MIMEAccept(Accept):

    """
    Represents the ``Accept`` header, which is a list of mimetypes.

    This class knows about mime wildcards, like ``image/*``
    """

    def _match(self, item, match):
        item = item.lower()
        if item == '*':
            item = '*/*'
        match = match.lower()
        if match == '*':
            match = '*/*'
        if '/' not in item:
            # Bad, but we ignore
            return False
        if '/' not in match:
            raise ValueError(
                "MIME matches must include / (bad: %r)" % match)
        item_major, item_minor = item.split('/', 1)
        match_major, match_minor = match.split('/', 1)
        if match_major == '*' and match_minor != '*':
            raise ValueError(
                "A MIME type of %r doesn't make sense" % match)
        if item_major == '*' and item_minor != '*':
            # Bad, but we ignore
            return False
        if ((item_major == '*' and item_minor == '*')
            or (match_major == '*' and match_minor == '*')):
            return True
        if (item_major == match_major
            and ((item_minor == '*' or match_minor == '*')
                 or item_minor == match_minor)):
            return True
        return False

    def accept_html(self):
        """
        Returns true if any HTML-like type is accepted
        """
        return ('text/html' in self
                or 'application/xhtml+xml' in self
                or 'application/xml' in self
                or 'text/xml' in self)

class MIMENilAccept(NilAccept):
    MasterClass = MIMEAccept