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