MoinSupport

ContentTypeSupport.py

46:99b3f4138cc2
2013-03-10 Paul Boddie Added macro argument parsing from MoinForms and a function to find parsers by content type. Changed the item storage API to use path tuples instead of strings when specifying directories. Introduced support for reverse iteration over stored items. Updated the release notes and version number.
     1 # -*- coding: iso-8859-1 -*-     2 """     3     MoinMoin - ContentTypeSupport library     4      5     @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk>     6     @license: GNU GPL (v2 or later), see COPYING.txt for details.     7 """     8      9 import re    10     11 # Content type parsing.    12     13 encoding_regexp_str = ur'(?P<content_type>[^\s;]*)(?:;\s*charset=(?P<encoding>[-A-Za-z0-9]+))?'    14 encoding_regexp = re.compile(encoding_regexp_str)    15     16 # Accept header parsing.    17     18 accept_regexp_str = ur';\s*q='    19 accept_regexp = re.compile(accept_regexp_str)    20     21 # Content/media type and preferences support.    22     23 class MediaRange:    24     25     "A content/media type value which supports whole categories of data."    26     27     def __init__(self, media_range, accept_parameters=None):    28         self.media_range = media_range    29         self.accept_parameters = accept_parameters or {}    30     31         parts = media_range.split(";")    32         self.media_type = parts[0]    33         self.parameters = getMappingFromParameterStrings(parts[1:])    34     35         # The media type is divided into category and subcategory.    36     37         parts = self.media_type.split("/")    38         self.category = parts[0]    39         self.subcategory = "/".join(parts[1:])    40     41     def get_parts(self):    42     43         "Return the category, subcategory parts."    44     45         return self.category, self.subcategory    46     47     def get_specificity(self):    48     49         """    50         Return the specificity of the media type in terms of the scope of the    51         category and subcategory, and also in terms of any qualifying    52         parameters.    53         """    54     55         if "*" in self.get_parts():    56             return -list(self.get_parts()).count("*")    57         else:    58             return len(self.parameters)    59     60     def permits(self, other):    61     62         """    63         Return whether this media type permits the use of the 'other' media type    64         if suggested as suitable content.    65         """    66     67         if not isinstance(other, MediaRange):    68             other = MediaRange(other)    69     70         category = categoryPermits(self.category, other.category)    71         subcategory = categoryPermits(self.subcategory, other.subcategory)    72     73         if category and subcategory:    74             if "*" not in (category, subcategory):    75                 return not self.parameters or self.parameters == other.parameters    76             else:    77                 return True    78         else:    79             return False    80     81     def __eq__(self, other):    82     83         """    84         Return whether this media type is effectively the same as the 'other'    85         media type.    86         """    87     88         if not isinstance(other, MediaRange):    89             other = MediaRange(other)    90     91         category = categoryMatches(self.category, other.category)    92         subcategory = categoryMatches(self.subcategory, other.subcategory)    93     94         if category and subcategory:    95             if "*" not in (category, subcategory):    96                 return self.parameters == other.parameters or \    97                     not self.parameters or not other.parameters    98             else:    99                 return True   100         else:   101             return False   102    103     def __ne__(self, other):   104         return not self.__eq__(other)   105    106     def __hash__(self):   107         return hash(self.media_range)   108    109     def __repr__(self):   110         return "MediaRange(%r)" % self.media_range   111    112 def categoryMatches(this, that):   113    114     """   115     Return the basis of a match between 'this' and 'that' or False if the given   116     categories do not match.   117     """   118    119     return (this == "*" or this == that) and this or \   120         that == "*" and that or False   121    122 def categoryPermits(this, that):   123    124     """   125     Return whether 'this' category permits 'that' category. Where 'this' is a   126     wildcard ("*"), 'that' should always match. A value of False is returned if   127     the categories do not otherwise match.   128     """   129    130     return (this == "*" or this == that) and this or False   131    132 def getMappingFromParameterStrings(l):   133    134     """   135     Return a mapping representing the list of "name=value" strings given by 'l'.   136     """   137    138     parameters = {}   139    140     for parameter in l:   141         parts = parameter.split("=")   142         name = parts[0].strip()   143         value = "=".join(parts[1:]).strip()   144         parameters[name] = value   145    146     return parameters   147    148 def getContentPreferences(accept):   149    150     """   151     Return a mapping from media types to parameters for content/media types   152     extracted from the given 'accept' header value. The mapping is returned in   153     the form of a list of (media type, parameters) tuples.   154    155     See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1   156     """   157    158     preferences = []   159    160     for field in accept.split(","):   161    162         # The media type with parameters (defined by the "media-range") is   163         # separated from any other parameters (defined as "accept-extension"   164         # parameters) by a quality parameter.   165    166         fparts = accept_regexp.split(field)   167    168         # The first part is always the media type.   169    170         media_type = fparts[0].strip()   171    172         # Any other parts can be interpreted as extension parameters.   173    174         if len(fparts) > 1:   175             fparts = ("q=" + ";q=".join(fparts[1:])).split(";")   176         else:   177             fparts = []   178    179         # Each field in the preferences can incorporate parameters separated by   180         # semicolon characters.   181    182         parameters = getMappingFromParameterStrings(fparts)   183         media_range = MediaRange(media_type, parameters)   184         preferences.append(media_range)   185    186     return ContentPreferences(preferences)   187    188 class ContentPreferences:   189    190     "A wrapper around content preference information."   191    192     def __init__(self, preferences):   193         self.preferences = preferences   194    195     def __iter__(self):   196         return iter(self.preferences)   197    198     def get_ordered(self, by_quality=0):   199    200         """   201         Return a list of content/media types in descending order of preference.   202         If 'by_quality' is set to a true value, the "q" value will be used as   203         the primary measure of preference; otherwise, only the specificity will   204         be considered.   205         """   206    207         ordered = {}   208    209         for media_range in self.preferences:   210             specificity = media_range.get_specificity()   211    212             if by_quality:   213                 q = float(media_range.accept_parameters.get("q", "1"))   214                 key = q, specificity   215             else:   216                 key = specificity   217    218             if not ordered.has_key(key):   219                 ordered[key] = []   220    221             ordered[key].append(media_range)   222    223         # Return the preferences in descending order of quality and specificity.   224    225         keys = ordered.keys()   226         keys.sort(reverse=True)   227         return [ordered[key] for key in keys]   228    229     def get_acceptable_types(self, available):   230    231         """   232         Return content/media types from those in the 'available' list supported   233         by the known preferences grouped by preference level in descending order   234         of preference.   235         """   236    237         matches = {}   238         available = set(available[:])   239    240         for level in self.get_ordered():   241             for media_range in level:   242    243                 # Attempt to match available types.   244    245                 found = set()   246                 for available_type in available:   247                     if media_range.permits(available_type):   248                         q = float(media_range.accept_parameters.get("q", "1"))   249                         if not matches.has_key(q):   250                             matches[q] = []   251                         matches[q].append(available_type)   252                         found.add(available_type)   253    254                 # Stop looking for matches for matched available types.   255    256                 if found:   257                     available.difference_update(found)   258    259         # Sort the matches in descending order of quality.   260    261         all_q = matches.keys()   262    263         if all_q:   264             all_q.sort(reverse=True)   265             return [matches[q] for q in all_q]   266         else:   267             return []   268    269     def get_preferred_types(self, available):   270    271         """   272         Return the preferred content/media types from those in the 'available'   273         list, given the known preferences.   274         """   275    276         preferred = self.get_acceptable_types(available)   277         if preferred:   278             return preferred[0]   279         else:   280             return []   281    282 # Content type parsing.   283    284 def getContentTypeAndEncoding(content_type):   285    286     """   287     Return a tuple with the content/media type and encoding, extracted from the   288     given 'content_type' header value.   289     """   290    291     m = encoding_regexp.search(content_type)   292     if m:   293         return m.group("content_type"), m.group("encoding")   294     else:   295         return None, None   296    297 # vim: tabstop=4 expandtab shiftwidth=4