1from __future__ import annotations 
    2 
    3import codecs 
    4import re 
    5 
    6from .structures import ImmutableList 
    7 
    8 
    9class Accept(ImmutableList): 
    10    """An :class:`Accept` object is just a list subclass for lists of 
    11    ``(value, quality)`` tuples.  It is automatically sorted by specificity 
    12    and quality. 
    13 
    14    All :class:`Accept` objects work similar to a list but provide extra 
    15    functionality for working with the data.  Containment checks are 
    16    normalized to the rules of that header: 
    17 
    18    >>> a = CharsetAccept([('ISO-8859-1', 1), ('utf-8', 0.7)]) 
    19    >>> a.best 
    20    'ISO-8859-1' 
    21    >>> 'iso-8859-1' in a 
    22    True 
    23    >>> 'UTF8' in a 
    24    True 
    25    >>> 'utf7' in a 
    26    False 
    27 
    28    To get the quality for an item you can use normal item lookup: 
    29 
    30    >>> print a['utf-8'] 
    31    0.7 
    32    >>> a['utf7'] 
    33    0 
    34 
    35    .. versionchanged:: 0.5 
    36       :class:`Accept` objects are forced immutable now. 
    37 
    38    .. versionchanged:: 1.0.0 
    39       :class:`Accept` internal values are no longer ordered 
    40       alphabetically for equal quality tags. Instead the initial 
    41       order is preserved. 
    42 
    43    """ 
    44 
    45    def __init__(self, values=()): 
    46        if values is None: 
    47            list.__init__(self) 
    48            self.provided = False 
    49        elif isinstance(values, Accept): 
    50            self.provided = values.provided 
    51            list.__init__(self, values) 
    52        else: 
    53            self.provided = True 
    54            values = sorted( 
    55                values, key=lambda x: (self._specificity(x[0]), x[1]), reverse=True 
    56            ) 
    57            list.__init__(self, values) 
    58 
    59    def _specificity(self, value): 
    60        """Returns a tuple describing the value's specificity.""" 
    61        return (value != "*",) 
    62 
    63    def _value_matches(self, value, item): 
    64        """Check if a value matches a given accept item.""" 
    65        return item == "*" or item.lower() == value.lower() 
    66 
    67    def __getitem__(self, key): 
    68        """Besides index lookup (getting item n) you can also pass it a string 
    69        to get the quality for the item.  If the item is not in the list, the 
    70        returned quality is ``0``. 
    71        """ 
    72        if isinstance(key, str): 
    73            return self.quality(key) 
    74        return list.__getitem__(self, key) 
    75 
    76    def quality(self, key): 
    77        """Returns the quality of the key. 
    78 
    79        .. versionadded:: 0.6 
    80           In previous versions you had to use the item-lookup syntax 
    81           (eg: ``obj[key]`` instead of ``obj.quality(key)``) 
    82        """ 
    83        for item, quality in self: 
    84            if self._value_matches(key, item): 
    85                return quality 
    86        return 0 
    87 
    88    def __contains__(self, value): 
    89        for item, _quality in self: 
    90            if self._value_matches(value, item): 
    91                return True 
    92        return False 
    93 
    94    def __repr__(self): 
    95        pairs_str = ", ".join(f"({x!r}, {y})" for x, y in self) 
    96        return f"{type(self).__name__}([{pairs_str}])" 
    97 
    98    def index(self, key): 
    99        """Get the position of an entry or raise :exc:`ValueError`. 
    100 
    101        :param key: The key to be looked up. 
    102 
    103        .. versionchanged:: 0.5 
    104           This used to raise :exc:`IndexError`, which was inconsistent 
    105           with the list API. 
    106        """ 
    107        if isinstance(key, str): 
    108            for idx, (item, _quality) in enumerate(self): 
    109                if self._value_matches(key, item): 
    110                    return idx 
    111            raise ValueError(key) 
    112        return list.index(self, key) 
    113 
    114    def find(self, key): 
    115        """Get the position of an entry or return -1. 
    116 
    117        :param key: The key to be looked up. 
    118        """ 
    119        try: 
    120            return self.index(key) 
    121        except ValueError: 
    122            return -1 
    123 
    124    def values(self): 
    125        """Iterate over all values.""" 
    126        for item in self: 
    127            yield item[0] 
    128 
    129    def to_header(self): 
    130        """Convert the header set into an HTTP header string.""" 
    131        result = [] 
    132        for value, quality in self: 
    133            if quality != 1: 
    134                value = f"{value};q={quality}" 
    135            result.append(value) 
    136        return ",".join(result) 
    137 
    138    def __str__(self): 
    139        return self.to_header() 
    140 
    141    def _best_single_match(self, match): 
    142        for client_item, quality in self: 
    143            if self._value_matches(match, client_item): 
    144                # self is sorted by specificity descending, we can exit 
    145                return client_item, quality 
    146        return None 
    147 
    148    def best_match(self, matches, default=None): 
    149        """Returns the best match from a list of possible matches based 
    150        on the specificity and quality of the client. If two items have the 
    151        same quality and specificity, the one is returned that comes first. 
    152 
    153        :param matches: a list of matches to check for 
    154        :param default: the value that is returned if none match 
    155        """ 
    156        result = default 
    157        best_quality = -1 
    158        best_specificity = (-1,) 
    159        for server_item in matches: 
    160            match = self._best_single_match(server_item) 
    161            if not match: 
    162                continue 
    163            client_item, quality = match 
    164            specificity = self._specificity(client_item) 
    165            if quality <= 0 or quality < best_quality: 
    166                continue 
    167            # better quality or same quality but more specific => better match 
    168            if quality > best_quality or specificity > best_specificity: 
    169                result = server_item 
    170                best_quality = quality 
    171                best_specificity = specificity 
    172        return result 
    173 
    174    @property 
    175    def best(self): 
    176        """The best match as value.""" 
    177        if self: 
    178            return self[0][0] 
    179 
    180 
    181_mime_split_re = re.compile(r"/|(?:\s*;\s*)") 
    182 
    183 
    184def _normalize_mime(value): 
    185    return _mime_split_re.split(value.lower()) 
    186 
    187 
    188class MIMEAccept(Accept): 
    189    """Like :class:`Accept` but with special methods and behavior for 
    190    mimetypes. 
    191    """ 
    192 
    193    def _specificity(self, value): 
    194        return tuple(x != "*" for x in _mime_split_re.split(value)) 
    195 
    196    def _value_matches(self, value, item): 
    197        # item comes from the client, can't match if it's invalid. 
    198        if "/" not in item: 
    199            return False 
    200 
    201        # value comes from the application, tell the developer when it 
    202        # doesn't look valid. 
    203        if "/" not in value: 
    204            raise ValueError(f"invalid mimetype {value!r}") 
    205 
    206        # Split the match value into type, subtype, and a sorted list of parameters. 
    207        normalized_value = _normalize_mime(value) 
    208        value_type, value_subtype = normalized_value[:2] 
    209        value_params = sorted(normalized_value[2:]) 
    210 
    211        # "*/*" is the only valid value that can start with "*". 
    212        if value_type == "*" and value_subtype != "*": 
    213            raise ValueError(f"invalid mimetype {value!r}") 
    214 
    215        # Split the accept item into type, subtype, and parameters. 
    216        normalized_item = _normalize_mime(item) 
    217        item_type, item_subtype = normalized_item[:2] 
    218        item_params = sorted(normalized_item[2:]) 
    219 
    220        # "*/not-*" from the client is invalid, can't match. 
    221        if item_type == "*" and item_subtype != "*": 
    222            return False 
    223 
    224        return ( 
    225            (item_type == "*" and item_subtype == "*") 
    226            or (value_type == "*" and value_subtype == "*") 
    227        ) or ( 
    228            item_type == value_type 
    229            and ( 
    230                item_subtype == "*" 
    231                or value_subtype == "*" 
    232                or (item_subtype == value_subtype and item_params == value_params) 
    233            ) 
    234        ) 
    235 
    236    @property 
    237    def accept_html(self): 
    238        """True if this object accepts HTML.""" 
    239        return ( 
    240            "text/html" in self or "application/xhtml+xml" in self or self.accept_xhtml 
    241        ) 
    242 
    243    @property 
    244    def accept_xhtml(self): 
    245        """True if this object accepts XHTML.""" 
    246        return "application/xhtml+xml" in self or "application/xml" in self 
    247 
    248    @property 
    249    def accept_json(self): 
    250        """True if this object accepts JSON.""" 
    251        return "application/json" in self 
    252 
    253 
    254_locale_delim_re = re.compile(r"[_-]") 
    255 
    256 
    257def _normalize_lang(value): 
    258    """Process a language tag for matching.""" 
    259    return _locale_delim_re.split(value.lower()) 
    260 
    261 
    262class LanguageAccept(Accept): 
    263    """Like :class:`Accept` but with normalization for language tags.""" 
    264 
    265    def _value_matches(self, value, item): 
    266        return item == "*" or _normalize_lang(value) == _normalize_lang(item) 
    267 
    268    def best_match(self, matches, default=None): 
    269        """Given a list of supported values, finds the best match from 
    270        the list of accepted values. 
    271 
    272        Language tags are normalized for the purpose of matching, but 
    273        are returned unchanged. 
    274 
    275        If no exact match is found, this will fall back to matching 
    276        the first subtag (primary language only), first with the 
    277        accepted values then with the match values. This partial is not 
    278        applied to any other language subtags. 
    279 
    280        The default is returned if no exact or fallback match is found. 
    281 
    282        :param matches: A list of supported languages to find a match. 
    283        :param default: The value that is returned if none match. 
    284        """ 
    285        # Look for an exact match first. If a client accepts "en-US", 
    286        # "en-US" is a valid match at this point. 
    287        result = super().best_match(matches) 
    288 
    289        if result is not None: 
    290            return result 
    291 
    292        # Fall back to accepting primary tags. If a client accepts 
    293        # "en-US", "en" is a valid match at this point. Need to use 
    294        # re.split to account for 2 or 3 letter codes. 
    295        fallback = Accept( 
    296            [(_locale_delim_re.split(item[0], 1)[0], item[1]) for item in self] 
    297        ) 
    298        result = fallback.best_match(matches) 
    299 
    300        if result is not None: 
    301            return result 
    302 
    303        # Fall back to matching primary tags. If the client accepts 
    304        # "en", "en-US" is a valid match at this point. 
    305        fallback_matches = [_locale_delim_re.split(item, 1)[0] for item in matches] 
    306        result = super().best_match(fallback_matches) 
    307 
    308        # Return a value from the original match list. Find the first 
    309        # original value that starts with the matched primary tag. 
    310        if result is not None: 
    311            return next(item for item in matches if item.startswith(result)) 
    312 
    313        return default 
    314 
    315 
    316class CharsetAccept(Accept): 
    317    """Like :class:`Accept` but with normalization for charsets.""" 
    318 
    319    def _value_matches(self, value, item): 
    320        def _normalize(name): 
    321            try: 
    322                return codecs.lookup(name).name 
    323            except LookupError: 
    324                return name.lower() 
    325 
    326        return item == "*" or _normalize(value) == _normalize(item)