Home | History | Annotate | Download | only in webob
      1 """
      2 Parses a variety of ``Accept-*`` headers.
      3 
      4 These headers generally take the form of::
      5 
      6     value1; q=0.5, value2; q=0
      7 
      8 Where the ``q`` parameter is optional.  In theory other parameters
      9 exists, but this ignores them.
     10 """
     11 
     12 import re
     13 
     14 from webob.headers import _trans_name as header_to_key
     15 from webob.util import (
     16     header_docstring,
     17     warn_deprecation,
     18     )
     19 
     20 part_re = re.compile(
     21     r',\s*([^\s;,\n]+)(?:[^,]*?;\s*q=([0-9.]*))?')
     22 
     23 
     24 
     25 
     26 def _warn_first_match():
     27     # TODO: remove .first_match in version 1.3
     28     warn_deprecation("Use best_match instead", '1.2', 3)
     29 
     30 class Accept(object):
     31     """
     32     Represents a generic ``Accept-*`` style header.
     33 
     34     This object should not be modified.  To add items you can use
     35     ``accept_obj + 'accept_thing'`` to get a new object
     36     """
     37 
     38     def __init__(self, header_value):
     39         self.header_value = header_value
     40         self._parsed = list(self.parse(header_value))
     41         self._parsed_nonzero = [(m,q) for (m,q) in self._parsed if q]
     42 
     43     @staticmethod
     44     def parse(value):
     45         """
     46         Parse ``Accept-*`` style header.
     47 
     48         Return iterator of ``(value, quality)`` pairs.
     49         ``quality`` defaults to 1.
     50         """
     51         for match in part_re.finditer(','+value):
     52             name = match.group(1)
     53             if name == 'q':
     54                 continue
     55             quality = match.group(2) or ''
     56             if quality:
     57                 try:
     58                     quality = max(min(float(quality), 1), 0)
     59                     yield (name, quality)
     60                     continue
     61                 except ValueError:
     62                     pass
     63             yield (name, 1)
     64 
     65     def __repr__(self):
     66         return '<%s(%r)>' % (self.__class__.__name__, str(self))
     67 
     68     def __iter__(self):
     69         for m,q in sorted(
     70             self._parsed_nonzero,
     71             key=lambda i: i[1],
     72             reverse=True
     73         ):
     74             yield m
     75 
     76     def __str__(self):
     77         result = []
     78         for mask, quality in self._parsed:
     79             if quality != 1:
     80                 mask = '%s;q=%0.*f' % (
     81                     mask, min(len(str(quality).split('.')[1]), 3), quality)
     82             result.append(mask)
     83         return ', '.join(result)
     84 
     85     def __add__(self, other, reversed=False):
     86         if isinstance(other, Accept):
     87             other = other.header_value
     88         if hasattr(other, 'items'):
     89             other = sorted(other.items(), key=lambda item: -item[1])
     90         if isinstance(other, (list, tuple)):
     91             result = []
     92             for item in other:
     93                 if isinstance(item, (list, tuple)):
     94                     name, quality = item
     95                     result.append('%s; q=%s' % (name, quality))
     96                 else:
     97                     result.append(item)
     98             other = ', '.join(result)
     99         other = str(other)
    100         my_value = self.header_value
    101         if reversed:
    102             other, my_value = my_value, other
    103         if not other:
    104             new_value = my_value
    105         elif not my_value:
    106             new_value = other
    107         else:
    108             new_value = my_value + ', ' + other
    109         return self.__class__(new_value)
    110 
    111     def __radd__(self, other):
    112         return self.__add__(other, True)
    113 
    114     def __contains__(self, offer):
    115         """
    116         Returns true if the given object is listed in the accepted
    117         types.
    118         """
    119         for mask, quality in self._parsed_nonzero:
    120             if self._match(mask, offer):
    121                 return True
    122 
    123     def quality(self, offer, modifier=1):
    124         """
    125         Return the quality of the given offer.  Returns None if there
    126         is no match (not 0).
    127         """
    128         bestq = 0
    129         for mask, q in self._parsed:
    130             if self._match(mask, offer):
    131                 bestq = max(bestq, q * modifier)
    132         return bestq or None
    133 
    134     def first_match(self, offers):
    135         """
    136         DEPRECATED
    137         Returns the first allowed offered type. Ignores quality.
    138         Returns the first offered type if nothing else matches; or if you include None
    139         at the end of the match list then that will be returned.
    140         """
    141         _warn_first_match()
    142 
    143     def best_match(self, offers, default_match=None):
    144         """
    145         Returns the best match in the sequence of offered types.
    146 
    147         The sequence can be a simple sequence, or you can have
    148         ``(match, server_quality)`` items in the sequence.  If you
    149         have these tuples then the client quality is multiplied by the
    150         server_quality to get a total.  If two matches have equal
    151         weight, then the one that shows up first in the `offers` list
    152         will be returned.
    153 
    154         But among matches with the same quality the match to a more specific
    155         requested type will be chosen. For example a match to text/* trumps */*.
    156 
    157         default_match (default None) is returned if there is no intersection.
    158         """
    159         best_quality = -1
    160         best_offer = default_match
    161         matched_by = '*/*'
    162         for offer in offers:
    163             if isinstance(offer, (tuple, list)):
    164                 offer, server_quality = offer
    165             else:
    166                 server_quality = 1
    167             for mask, quality in self._parsed_nonzero:
    168                 possible_quality = server_quality * quality
    169                 if possible_quality < best_quality:
    170                     continue
    171                 elif possible_quality == best_quality:
    172                     # 'text/plain' overrides 'message/*' overrides '*/*'
    173                     # (if all match w/ the same q=)
    174                     if matched_by.count('*') <= mask.count('*'):
    175                         continue
    176                 if self._match(mask, offer):
    177                     best_quality = possible_quality
    178                     best_offer = offer
    179                     matched_by = mask
    180         return best_offer
    181 
    182     def _match(self, mask, offer):
    183         _check_offer(offer)
    184         return mask == '*' or offer.lower() == mask.lower()
    185 
    186 
    187 
    188 class NilAccept(object):
    189     MasterClass = Accept
    190 
    191     def __repr__(self):
    192         return '<%s: %s>' % (self.__class__.__name__, self.MasterClass)
    193 
    194     def __str__(self):
    195         return ''
    196 
    197     def __nonzero__(self):
    198         return False
    199     __bool__ = __nonzero__ # python 3
    200 
    201     def __iter__(self):
    202         return iter(())
    203 
    204     def __add__(self, item):
    205         if isinstance(item, self.MasterClass):
    206             return item
    207         else:
    208             return self.MasterClass('') + item
    209 
    210     def __radd__(self, item):
    211         if isinstance(item, self.MasterClass):
    212             return item
    213         else:
    214             return item + self.MasterClass('')
    215 
    216     def __contains__(self, item):
    217         _check_offer(item)
    218         return True
    219 
    220     def quality(self, offer, default_quality=1):
    221         return 0
    222 
    223     def first_match(self, offers): # pragma: no cover
    224         _warn_first_match()
    225 
    226     def best_match(self, offers, default_match=None):
    227         best_quality = -1
    228         best_offer = default_match
    229         for offer in offers:
    230             _check_offer(offer)
    231             if isinstance(offer, (list, tuple)):
    232                 offer, quality = offer
    233             else:
    234                 quality = 1
    235             if quality > best_quality:
    236                 best_offer = offer
    237                 best_quality = quality
    238         return best_offer
    239 
    240 class NoAccept(NilAccept):
    241     def __contains__(self, item):
    242         return False
    243 
    244 class AcceptCharset(Accept):
    245     @staticmethod
    246     def parse(value):
    247         latin1_found = False
    248         for m, q in Accept.parse(value):
    249             _m = m.lower()
    250             if _m == '*' or _m == 'iso-8859-1':
    251                 latin1_found = True
    252             yield _m, q
    253         if not latin1_found:
    254             yield ('iso-8859-1', 1)
    255 
    256 class AcceptLanguage(Accept):
    257     def _match(self, mask, item):
    258         item = item.replace('_', '-').lower()
    259         mask = mask.lower()
    260         return (mask == '*'
    261             or item == mask
    262             or item.split('-')[0] == mask
    263             or item == mask.split('-')[0]
    264         )
    265 
    266 
    267 class MIMEAccept(Accept):
    268     """
    269         Represents the ``Accept`` header, which is a list of mimetypes.
    270 
    271         This class knows about mime wildcards, like ``image/*``
    272     """
    273     @staticmethod
    274     def parse(value):
    275         for mask, q in Accept.parse(value):
    276             try:
    277                 mask_major, mask_minor = map(lambda x: x.lower(), mask.split('/'))
    278             except ValueError:
    279                 continue
    280             if mask_major == '*' and mask_minor != '*':
    281                 continue
    282             if mask_major != "*" and "*" in mask_major:
    283                 continue
    284             if mask_minor != "*" and "*" in mask_minor:
    285                 continue
    286             yield ("%s/%s" % (mask_major, mask_minor), q)
    287 
    288     def accept_html(self):
    289         """
    290         Returns true if any HTML-like type is accepted
    291         """
    292         return ('text/html' in self
    293                 or 'application/xhtml+xml' in self
    294                 or 'application/xml' in self
    295                 or 'text/xml' in self)
    296 
    297     accepts_html = property(accept_html) # note the plural
    298 
    299     def _match(self, mask, offer):
    300         """
    301             Check if the offer is covered by the mask
    302         """
    303         _check_offer(offer)
    304         if '*' not in mask:
    305             return offer.lower() == mask.lower()
    306         elif mask == '*/*':
    307             return True
    308         else:
    309             assert mask.endswith('/*')
    310             mask_major = mask[:-2].lower()
    311             offer_major = offer.split('/', 1)[0].lower()
    312             return offer_major == mask_major
    313 
    314 
    315 class MIMENilAccept(NilAccept):
    316     MasterClass = MIMEAccept
    317 
    318 def _check_offer(offer):
    319     if '*' in offer:
    320         raise ValueError("The application should offer specific types, got %r" % offer)
    321 
    322 
    323 
    324 def accept_property(header, rfc_section,
    325     AcceptClass=Accept, NilClass=NilAccept
    326 ):
    327     key = header_to_key(header)
    328     doc = header_docstring(header, rfc_section)
    329     #doc += "  Converts it as a %s." % convert_name
    330     def fget(req):
    331         value = req.environ.get(key)
    332         if not value:
    333             return NilClass()
    334         return AcceptClass(value)
    335     def fset(req, val):
    336         if val:
    337             if isinstance(val, (list, tuple, dict)):
    338                 val = AcceptClass('') + val
    339             val = str(val)
    340         req.environ[key] = val or None
    341     def fdel(req):
    342         del req.environ[key]
    343     return property(fget, fset, fdel, doc)
    344