0001"""
0002Parses a variety of ``Accept-*`` headers.
0003
0004These headers generally take the form of::
0005
0006    value1; q=0.5, value2; q=0
0007
0008Where the ``q`` parameter is optional.  In theory other parameters
0009exists, but this ignores them.
0010"""
0011
0012import re
0013try:
0014    sorted
0015except NameError:
0016    from webob.compat import sorted
0017
0018part_re = re.compile(
0019    r',\s*([^\s;,\n]+)(?:[^,]*?;\s*q=([0-9.]*))?')
0020
0021def parse_accept(value):
0022    """
0023    Parses an ``Accept-*`` style header.
0024
0025    A list of ``[(value, quality), ...]`` is returned.  ``quality``
0026    will be 1 if it was not given.
0027    """
0028    result = []
0029    for match in part_re.finditer(','+value):
0030        name = match.group(1)
0031        if name == 'q':
0032            continue
0033        quality = match.group(2) or ''
0034        if not quality:
0035            quality = 1
0036        else:
0037            try:
0038                quality = max(min(float(quality), 1), 0)
0039            except ValueError:
0040                quality = 1
0041        result.append((name, quality))
0042    return result
0043
0044class Accept(object):
0045    """
0046    Represents a generic ``Accept-*`` style header.
0047
0048    This object should not be modified.  To add items you can use
0049    ``accept_obj + 'accept_thing'`` to get a new object
0050    """
0051
0052    def __init__(self, header_name, header_value):
0053        self.header_name = header_name
0054        self.header_value = header_value
0055        self._parsed = parse_accept(header_value)
0056
0057    def __repr__(self):
0058        return '<%s at %x %s: %s>' % (
0059            self.__class__.__name__,
0060            abs(id(self)),
0061            self.header_name, str(self))
0062
0063    def __str__(self):
0064        result = []
0065        for match, quality in self._parsed:
0066            if quality != 1:
0067                match = '%s;q=%0.1f' % (match, quality)
0068            result.append(match)
0069        return ', '.join(result)
0070
0071    # FIXME: should subtraction be allowed?
0072    def __add__(self, other, reversed=False):
0073        if isinstance(other, Accept):
0074            other = other.header_value
0075        if hasattr(other, 'items'):
0076            other = sorted(other.items(), key=lambda item: -item[1])
0077        if isinstance(other, (list, tuple)):
0078            result = []
0079            for item in other:
0080                if isinstance(item, (list, tuple)):
0081                    name, quality = item
0082                    result.append('%s; q=%s' % (name, quality))
0083                else:
0084                    result.append(item)
0085            other = ', '.join(result)
0086        other = str(other)
0087        my_value = self.header_value
0088        if reversed:
0089            other, my_value = my_value, other
0090        if not other:
0091            new_value = my_value
0092        elif not my_value:
0093            new_value = other
0094        else:
0095            new_value = my_value + ', ' + other
0096        return self.__class__(self.header_name, new_value)
0097
0098    def __radd__(self, other):
0099        return self.__add__(other, True)
0100
0101    def __contains__(self, match):
0102        """
0103        Returns true if the given object is listed in the accepted
0104        types.
0105        """
0106        for item, quality in self._parsed:
0107            if self._match(item, match):
0108                return True
0109
0110    def quality(self, match):
0111        """
0112        Return the quality of the given match.  Returns None if there
0113        is no match (not 0).
0114        """
0115        for item, quality in self._parsed:
0116            if self._match(item, match):
0117                return quality
0118        return None
0119
0120    def first_match(self, matches):
0121        """
0122        Returns the first match in the sequences of matches that is
0123        allowed.  Ignores quality.  Returns the first item if nothing
0124        else matches; or if you include None at the end of the match
0125        list then that will be returned.
0126        """
0127        if not matches:
0128            raise ValueError(
0129                "You must pass in a non-empty list")
0130        for match in matches:
0131            for item, quality in self._parsed:
0132                if self._match(item, match):
0133                    return match
0134            if match is None:
0135                return None
0136        return matches[0]
0137
0138    def best_match(self, matches, default_match=None):
0139        """
0140        Returns the best match in the sequence of matches.
0141
0142        The sequence can be a simple sequence, or you can have
0143        ``(match, server_quality)`` items in the sequence.  If you
0144        have these tuples then the client quality is multiplied by the
0145        server_quality to get a total.
0146
0147        default_match (default None) is returned if there is no intersection.
0148        """
0149        best_quality = -1
0150        best_match = default_match
0151        for match_item in matches:
0152            if isinstance(match_item, (tuple, list)):
0153                match, server_quality = match_item
0154            else:
0155                match = match_item
0156                server_quality = 1
0157            for item, quality in self._parsed:
0158                possible_quality = server_quality * quality
0159                if possible_quality < best_quality:
0160                    continue
0161                if self._match(item, match):
0162                    best_quality = possible_quality
0163                    best_match = match
0164        return best_match
0165
0166    def best_matches(self, fallback=None):
0167        """
0168        Return all the matches in order of quality, with fallback (if
0169        given) at the end.
0170        """
0171        items = [
0172            i for i, q in sorted(self._parsed, key=lambda iq: -iq[1])]
0173        if fallback:
0174            for index, item in enumerate(items):
0175                if self._match(item, fallback):
0176                    items[index+1:] = []
0177                    break
0178            else:
0179                items.append(fallback)
0180        return items
0181
0182    def _match(self, item, match):
0183        return item.lower() == match.lower() or item == '*'
0184
0185class NilAccept(object):
0186
0187    """
0188    Represents an Accept header with no value.
0189    """
0190
0191    MasterClass = Accept
0192
0193    def __init__(self, header_name):
0194        self.header_name = header_name
0195
0196    def __repr__(self):
0197        return '<%s for %s: %s>' % (
0198            self.__class__.__name__, self.header_name, self.MasterClass)
0199
0200    def __str__(self):
0201        return ''
0202
0203    def __add__(self, item):
0204        if isinstance(item, self.MasterClass):
0205            return item
0206        else:
0207            return self.MasterClass(self.header_name, '') + item
0208
0209    def __radd__(self, item):
0210        if isinstance(item, self.MasterClass):
0211            return item
0212        else:
0213            return item + self.MasterClass(self.header_name, '')
0214
0215    def __contains__(self, item):
0216        return True
0217
0218    def quality(self, match, default_quality=1):
0219        return 0
0220
0221    def first_match(self, matches):
0222        return matches[0]
0223
0224    def best_match(self, matches, default_match=None):
0225        best_quality = -1
0226        best_match = default_match
0227        for match_item in matches:
0228            if isinstance(match_item, (list, tuple)):
0229                match, quality = match_item
0230            else:
0231                match = match_item
0232                quality = 1
0233            if quality > best_quality:
0234                best_match = match
0235                best_quality = quality
0236        return best_match
0237
0238    def best_matches(self, fallback=None):
0239        if fallback:
0240            return [fallback]
0241        else:
0242            return []
0243
0244class NoAccept(NilAccept):
0245
0246    def __contains__(self, item):
0247        return False
0248
0249class MIMEAccept(Accept):
0250
0251    """
0252    Represents the ``Accept`` header, which is a list of mimetypes.
0253
0254    This class knows about mime wildcards, like ``image/*``
0255    """
0256
0257    def _match(self, item, match):
0258        item = item.lower()
0259        if item == '*':
0260            item = '*/*'
0261        match = match.lower()
0262        if match == '*':
0263            match = '*/*'
0264        if '/' not in item:
0265            # Bad, but we ignore
0266            return False
0267        if '/' not in match:
0268            raise ValueError(
0269                "MIME matches must include / (bad: %r)" % match)
0270        item_major, item_minor = item.split('/', 1)
0271        match_major, match_minor = match.split('/', 1)
0272        if match_major == '*' and match_minor != '*':
0273            raise ValueError(
0274                "A MIME type of %r doesn't make sense" % match)
0275        if item_major == '*' and item_minor != '*':
0276            # Bad, but we ignore
0277            return False
0278        if ((item_major == '*' and item_minor == '*')
0279            or (match_major == '*' and match_minor == '*')):
0280            return True
0281        if (item_major == match_major
0282            and ((item_minor == '*' or match_minor == '*')
0283                 or item_minor == match_minor)):
0284            return True
0285        return False
0286
0287    def accept_html(self):
0288        """
0289        Returns true if any HTML-like type is accepted
0290        """
0291        return ('text/html' in self
0292                or 'application/xhtml+xml' in self
0293                or 'application/xml' in self
0294                or 'text/xml' in self)
0295
0296class MIMENilAccept(NilAccept):
0297    MasterClass = MIMEAccept