thirdparty/google_appengine/lib/webob/webob/acceptparse.py
author Pawel Solyga <Pawel.Solyga@gmail.com>
Fri, 07 Nov 2008 22:24:01 +0000
changeset 448 075360be6743
parent 109 620f9b141567
permissions -rwxr-xr-x
Fix not working former_ids. Add support for "Invalid accounts". Now when id from former_ids tries to create a profile "This account is invalid." error message is displayed. Compare emails in lower cases to prevent changing User email to the same email with different character casing (needs some more testing). Patch by: Pawel Solyga

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