1"""
2Parses a variety of ``Accept-*`` headers.
3
4These headers generally take the form of::
5
6    value1; q=0.5, value2; q=0
7
8Where the ``q`` parameter is optional.  In theory other parameters
9exists, but this ignores them.
10"""
11
12import re
13
14from webob.headers import _trans_name as header_to_key
15from webob.util import (
16    header_docstring,
17    warn_deprecation,
18    )
19
20part_re = re.compile(
21    r',\s*([^\s;,\n]+)(?:[^,]*?;\s*q=([0-9.]*))?')
22
23
24
25
26def _warn_first_match():
27    # TODO: remove .first_match in version 1.3
28    warn_deprecation("Use best_match instead", '1.2', 3)
29
30class 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
188class 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
240class NoAccept(NilAccept):
241    def __contains__(self, item):
242        return False
243
244class 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
256class 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
267class 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
315class MIMENilAccept(NilAccept):
316    MasterClass = MIMEAccept
317
318def _check_offer(offer):
319    if '*' in offer:
320        raise ValueError("The application should offer specific types, got %r" % offer)
321
322
323
324def 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