| """ |
| 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 |
| |
| from webob.headers import _trans_name as header_to_key |
| from webob.util import ( |
| header_docstring, |
| warn_deprecation, |
| ) |
| |
| part_re = re.compile( |
| r',\s*([^\s;,\n]+)(?:[^,]*?;\s*q=([0-9.]*))?') |
| |
| |
| |
| |
| def _warn_first_match(): |
| # TODO: remove .first_match in version 1.3 |
| warn_deprecation("Use best_match instead", '1.2', 3) |
| |
| 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_value): |
| self.header_value = header_value |
| self._parsed = list(self.parse(header_value)) |
| self._parsed_nonzero = [(m,q) for (m,q) in self._parsed if q] |
| |
| @staticmethod |
| def parse(value): |
| """ |
| Parse ``Accept-*`` style header. |
| |
| Return iterator of ``(value, quality)`` pairs. |
| ``quality`` defaults to 1. |
| """ |
| for match in part_re.finditer(','+value): |
| name = match.group(1) |
| if name == 'q': |
| continue |
| quality = match.group(2) or '' |
| if quality: |
| try: |
| quality = max(min(float(quality), 1), 0) |
| yield (name, quality) |
| continue |
| except ValueError: |
| pass |
| yield (name, 1) |
| |
| def __repr__(self): |
| return '<%s(%r)>' % (self.__class__.__name__, str(self)) |
| |
| def __iter__(self): |
| for m,q in sorted( |
| self._parsed_nonzero, |
| key=lambda i: i[1], |
| reverse=True |
| ): |
| yield m |
| |
| def __str__(self): |
| result = [] |
| for mask, quality in self._parsed: |
| if quality != 1: |
| mask = '%s;q=%0.*f' % ( |
| mask, min(len(str(quality).split('.')[1]), 3), quality) |
| result.append(mask) |
| return ', '.join(result) |
| |
| 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__(new_value) |
| |
| def __radd__(self, other): |
| return self.__add__(other, True) |
| |
| def __contains__(self, offer): |
| """ |
| Returns true if the given object is listed in the accepted |
| types. |
| """ |
| for mask, quality in self._parsed_nonzero: |
| if self._match(mask, offer): |
| return True |
| |
| def quality(self, offer, modifier=1): |
| """ |
| Return the quality of the given offer. Returns None if there |
| is no match (not 0). |
| """ |
| bestq = 0 |
| for mask, q in self._parsed: |
| if self._match(mask, offer): |
| bestq = max(bestq, q * modifier) |
| return bestq or None |
| |
| def first_match(self, offers): |
| """ |
| DEPRECATED |
| Returns the first allowed offered type. Ignores quality. |
| Returns the first offered type if nothing else matches; or if you include None |
| at the end of the match list then that will be returned. |
| """ |
| _warn_first_match() |
| |
| def best_match(self, offers, default_match=None): |
| """ |
| Returns the best match in the sequence of offered types. |
| |
| 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. If two matches have equal |
| weight, then the one that shows up first in the `offers` list |
| will be returned. |
| |
| But among matches with the same quality the match to a more specific |
| requested type will be chosen. For example a match to text/* trumps */*. |
| |
| default_match (default None) is returned if there is no intersection. |
| """ |
| best_quality = -1 |
| best_offer = default_match |
| matched_by = '*/*' |
| for offer in offers: |
| if isinstance(offer, (tuple, list)): |
| offer, server_quality = offer |
| else: |
| server_quality = 1 |
| for mask, quality in self._parsed_nonzero: |
| possible_quality = server_quality * quality |
| if possible_quality < best_quality: |
| continue |
| elif possible_quality == best_quality: |
| # 'text/plain' overrides 'message/*' overrides '*/*' |
| # (if all match w/ the same q=) |
| if matched_by.count('*') <= mask.count('*'): |
| continue |
| if self._match(mask, offer): |
| best_quality = possible_quality |
| best_offer = offer |
| matched_by = mask |
| return best_offer |
| |
| def _match(self, mask, offer): |
| _check_offer(offer) |
| return mask == '*' or offer.lower() == mask.lower() |
| |
| |
| |
| class NilAccept(object): |
| MasterClass = Accept |
| |
| def __repr__(self): |
| return '<%s: %s>' % (self.__class__.__name__, self.MasterClass) |
| |
| def __str__(self): |
| return '' |
| |
| def __nonzero__(self): |
| return False |
| __bool__ = __nonzero__ # python 3 |
| |
| def __iter__(self): |
| return iter(()) |
| |
| def __add__(self, item): |
| if isinstance(item, self.MasterClass): |
| return item |
| else: |
| return self.MasterClass('') + item |
| |
| def __radd__(self, item): |
| if isinstance(item, self.MasterClass): |
| return item |
| else: |
| return item + self.MasterClass('') |
| |
| def __contains__(self, item): |
| _check_offer(item) |
| return True |
| |
| def quality(self, offer, default_quality=1): |
| return 0 |
| |
| def first_match(self, offers): # pragma: no cover |
| _warn_first_match() |
| |
| def best_match(self, offers, default_match=None): |
| best_quality = -1 |
| best_offer = default_match |
| for offer in offers: |
| _check_offer(offer) |
| if isinstance(offer, (list, tuple)): |
| offer, quality = offer |
| else: |
| quality = 1 |
| if quality > best_quality: |
| best_offer = offer |
| best_quality = quality |
| return best_offer |
| |
| class NoAccept(NilAccept): |
| def __contains__(self, item): |
| return False |
| |
| class AcceptCharset(Accept): |
| @staticmethod |
| def parse(value): |
| latin1_found = False |
| for m, q in Accept.parse(value): |
| _m = m.lower() |
| if _m == '*' or _m == 'iso-8859-1': |
| latin1_found = True |
| yield _m, q |
| if not latin1_found: |
| yield ('iso-8859-1', 1) |
| |
| class AcceptLanguage(Accept): |
| def _match(self, mask, item): |
| item = item.replace('_', '-').lower() |
| mask = mask.lower() |
| return (mask == '*' |
| or item == mask |
| or item.split('-')[0] == mask |
| or item == mask.split('-')[0] |
| ) |
| |
| |
| class MIMEAccept(Accept): |
| """ |
| Represents the ``Accept`` header, which is a list of mimetypes. |
| |
| This class knows about mime wildcards, like ``image/*`` |
| """ |
| @staticmethod |
| def parse(value): |
| for mask, q in Accept.parse(value): |
| try: |
| mask_major, mask_minor = map(lambda x: x.lower(), mask.split('/')) |
| except ValueError: |
| continue |
| if mask_major == '*' and mask_minor != '*': |
| continue |
| if mask_major != "*" and "*" in mask_major: |
| continue |
| if mask_minor != "*" and "*" in mask_minor: |
| continue |
| yield ("%s/%s" % (mask_major, mask_minor), q) |
| |
| 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) |
| |
| accepts_html = property(accept_html) # note the plural |
| |
| def _match(self, mask, offer): |
| """ |
| Check if the offer is covered by the mask |
| """ |
| _check_offer(offer) |
| if '*' not in mask: |
| return offer.lower() == mask.lower() |
| elif mask == '*/*': |
| return True |
| else: |
| assert mask.endswith('/*') |
| mask_major = mask[:-2].lower() |
| offer_major = offer.split('/', 1)[0].lower() |
| return offer_major == mask_major |
| |
| |
| class MIMENilAccept(NilAccept): |
| MasterClass = MIMEAccept |
| |
| def _check_offer(offer): |
| if '*' in offer: |
| raise ValueError("The application should offer specific types, got %r" % offer) |
| |
| |
| |
| def accept_property(header, rfc_section, |
| AcceptClass=Accept, NilClass=NilAccept |
| ): |
| key = header_to_key(header) |
| doc = header_docstring(header, rfc_section) |
| #doc += " Converts it as a %s." % convert_name |
| def fget(req): |
| value = req.environ.get(key) |
| if not value: |
| return NilClass() |
| return AcceptClass(value) |
| def fset(req, val): |
| if val: |
| if isinstance(val, (list, tuple, dict)): |
| val = AcceptClass('') + val |
| val = str(val) |
| req.environ[key] = val or None |
| def fdel(req): |
| del req.environ[key] |
| return property(fget, fset, fdel, doc) |