# HG changeset patch # User Paul Boddie # Date 1358608109 -3600 # Node ID 367c9d96bc2aff275adf9044ca0f4fe60dda8111 # Parent 4bb7e573787369c6803c85c6ce836bbcd2f83c84 Moved content/media type functions into a separate module. diff -r 4bb7e5737873 -r 367c9d96bc2a ContentTypeSupport.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/ContentTypeSupport.py Sat Jan 19 16:08:29 2013 +0100 @@ -0,0 +1,297 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - ContentTypeSupport library + + @copyright: 2012, 2013 by Paul Boddie + @license: GNU GPL (v2 or later), see COPYING.txt for details. +""" + +import re + +# Content type parsing. + +encoding_regexp_str = ur'(?P[^\s;]*)(?:;\s*charset=(?P[-A-Za-z0-9]+))?' +encoding_regexp = re.compile(encoding_regexp_str) + +# Accept header parsing. + +accept_regexp_str = ur';\s*q=' +accept_regexp = re.compile(accept_regexp_str) + +# Content/media type and preferences support. + +class MediaRange: + + "A content/media type value which supports whole categories of data." + + def __init__(self, media_range, accept_parameters=None): + self.media_range = media_range + self.accept_parameters = accept_parameters or {} + + parts = media_range.split(";") + self.media_type = parts[0] + self.parameters = getMappingFromParameterStrings(parts[1:]) + + # The media type is divided into category and subcategory. + + parts = self.media_type.split("/") + self.category = parts[0] + self.subcategory = "/".join(parts[1:]) + + def get_parts(self): + + "Return the category, subcategory parts." + + return self.category, self.subcategory + + def get_specificity(self): + + """ + Return the specificity of the media type in terms of the scope of the + category and subcategory, and also in terms of any qualifying + parameters. + """ + + if "*" in self.get_parts(): + return -list(self.get_parts()).count("*") + else: + return len(self.parameters) + + def permits(self, other): + + """ + Return whether this media type permits the use of the 'other' media type + if suggested as suitable content. + """ + + if not isinstance(other, MediaRange): + other = MediaRange(other) + + category = categoryPermits(self.category, other.category) + subcategory = categoryPermits(self.subcategory, other.subcategory) + + if category and subcategory: + if "*" not in (category, subcategory): + return not self.parameters or self.parameters == other.parameters + else: + return True + else: + return False + + def __eq__(self, other): + + """ + Return whether this media type is effectively the same as the 'other' + media type. + """ + + if not isinstance(other, MediaRange): + other = MediaRange(other) + + category = categoryMatches(self.category, other.category) + subcategory = categoryMatches(self.subcategory, other.subcategory) + + if category and subcategory: + if "*" not in (category, subcategory): + return self.parameters == other.parameters or \ + not self.parameters or not other.parameters + else: + return True + else: + return False + + def __ne__(self, other): + return not self.__eq__(other) + + def __hash__(self): + return hash(self.media_range) + + def __repr__(self): + return "MediaRange(%r)" % self.media_range + +def categoryMatches(this, that): + + """ + Return the basis of a match between 'this' and 'that' or False if the given + categories do not match. + """ + + return (this == "*" or this == that) and this or \ + that == "*" and that or False + +def categoryPermits(this, that): + + """ + Return whether 'this' category permits 'that' category. Where 'this' is a + wildcard ("*"), 'that' should always match. A value of False is returned if + the categories do not otherwise match. + """ + + return (this == "*" or this == that) and this or False + +def getMappingFromParameterStrings(l): + + """ + Return a mapping representing the list of "name=value" strings given by 'l'. + """ + + parameters = {} + + for parameter in l: + parts = parameter.split("=") + name = parts[0].strip() + value = "=".join(parts[1:]).strip() + parameters[name] = value + + return parameters + +def getContentPreferences(accept): + + """ + Return a mapping from media types to parameters for content/media types + extracted from the given 'accept' header value. The mapping is returned in + the form of a list of (media type, parameters) tuples. + + See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 + """ + + preferences = [] + + for field in accept.split(","): + + # The media type with parameters (defined by the "media-range") is + # separated from any other parameters (defined as "accept-extension" + # parameters) by a quality parameter. + + fparts = accept_regexp.split(field) + + # The first part is always the media type. + + media_type = fparts[0].strip() + + # Any other parts can be interpreted as extension parameters. + + if len(fparts) > 1: + fparts = ("q=" + ";q=".join(fparts[1:])).split(";") + else: + fparts = [] + + # Each field in the preferences can incorporate parameters separated by + # semicolon characters. + + parameters = getMappingFromParameterStrings(fparts) + media_range = MediaRange(media_type, parameters) + preferences.append(media_range) + + return ContentPreferences(preferences) + +class ContentPreferences: + + "A wrapper around content preference information." + + def __init__(self, preferences): + self.preferences = preferences + + def __iter__(self): + return iter(self.preferences) + + def get_ordered(self, by_quality=0): + + """ + Return a list of content/media types in descending order of preference. + If 'by_quality' is set to a true value, the "q" value will be used as + the primary measure of preference; otherwise, only the specificity will + be considered. + """ + + ordered = {} + + for media_range in self.preferences: + specificity = media_range.get_specificity() + + if by_quality: + q = float(media_range.accept_parameters.get("q", "1")) + key = q, specificity + else: + key = specificity + + if not ordered.has_key(key): + ordered[key] = [] + + ordered[key].append(media_range) + + # Return the preferences in descending order of quality and specificity. + + keys = ordered.keys() + keys.sort(reverse=True) + return [ordered[key] for key in keys] + + def get_acceptable_types(self, available): + + """ + Return content/media types from those in the 'available' list supported + by the known preferences grouped by preference level in descending order + of preference. + """ + + matches = {} + available = set(available[:]) + + for level in self.get_ordered(): + for media_range in level: + + # Attempt to match available types. + + found = set() + for available_type in available: + if media_range.permits(available_type): + q = float(media_range.accept_parameters.get("q", "1")) + if not matches.has_key(q): + matches[q] = [] + matches[q].append(available_type) + found.add(available_type) + + # Stop looking for matches for matched available types. + + if found: + available.difference_update(found) + + # Sort the matches in descending order of quality. + + all_q = matches.keys() + + if all_q: + all_q.sort(reverse=True) + return [matches[q] for q in all_q] + else: + return [] + + def get_preferred_types(self, available): + + """ + Return the preferred content/media types from those in the 'available' + list, given the known preferences. + """ + + preferred = self.get_acceptable_types(available) + if preferred: + return preferred[0] + else: + return [] + +# Content type parsing. + +def getContentTypeAndEncoding(content_type): + + """ + Return a tuple with the content/media type and encoding, extracted from the + given 'content_type' header value. + """ + + m = encoding_regexp.search(content_type) + if m: + return m.group("content_type"), m.group("encoding") + else: + return None, None + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 4bb7e5737873 -r 367c9d96bc2a MoinSupport.py --- a/MoinSupport.py Fri Jan 04 22:57:15 2013 +0100 +++ b/MoinSupport.py Sat Jan 19 16:08:29 2013 +0100 @@ -2,7 +2,7 @@ """ MoinMoin - MoinSupport library (derived from EventAggregatorSupport) - @copyright: 2008, 2009, 2010, 2011, 2012 by Paul Boddie + @copyright: 2008, 2009, 2010, 2011, 2012, 2013 by Paul Boddie @copyright: 2000-2004 Juergen Hermann , 2005-2008 MoinMoin:ThomasWaldmann. @license: GNU GPL (v2 or later), see COPYING.txt for details. @@ -25,16 +25,6 @@ __version__ = "0.2" -# Content type parsing. - -encoding_regexp_str = ur'(?P[^\s;]*)(?:;\s*charset=(?P[-A-Za-z0-9]+))?' -encoding_regexp = re.compile(encoding_regexp_str) - -# Accept header parsing. - -accept_regexp_str = ur';\s*q=' -accept_regexp = re.compile(accept_regexp_str) - # Extraction of shared fragments. marker_regexp_str = r"([{]{3,}|[}]{3,})" @@ -625,282 +615,6 @@ send_headers(headers) -# Content/media type and preferences support. - -class MediaRange: - - "A content/media type value which supports whole categories of data." - - def __init__(self, media_range, accept_parameters=None): - self.media_range = media_range - self.accept_parameters = accept_parameters or {} - - parts = media_range.split(";") - self.media_type = parts[0] - self.parameters = getMappingFromParameterStrings(parts[1:]) - - # The media type is divided into category and subcategory. - - parts = self.media_type.split("/") - self.category = parts[0] - self.subcategory = "/".join(parts[1:]) - - def get_parts(self): - - "Return the category, subcategory parts." - - return self.category, self.subcategory - - def get_specificity(self): - - """ - Return the specificity of the media type in terms of the scope of the - category and subcategory, and also in terms of any qualifying - parameters. - """ - - if "*" in self.get_parts(): - return -list(self.get_parts()).count("*") - else: - return len(self.parameters) - - def permits(self, other): - - """ - Return whether this media type permits the use of the 'other' media type - if suggested as suitable content. - """ - - if not isinstance(other, MediaRange): - other = MediaRange(other) - - category = categoryPermits(self.category, other.category) - subcategory = categoryPermits(self.subcategory, other.subcategory) - - if category and subcategory: - if "*" not in (category, subcategory): - return not self.parameters or self.parameters == other.parameters - else: - return True - else: - return False - - def __eq__(self, other): - - """ - Return whether this media type is effectively the same as the 'other' - media type. - """ - - if not isinstance(other, MediaRange): - other = MediaRange(other) - - category = categoryMatches(self.category, other.category) - subcategory = categoryMatches(self.subcategory, other.subcategory) - - if category and subcategory: - if "*" not in (category, subcategory): - return self.parameters == other.parameters or \ - not self.parameters or not other.parameters - else: - return True - else: - return False - - def __ne__(self, other): - return not self.__eq__(other) - - def __hash__(self): - return hash(self.media_range) - - def __repr__(self): - return "MediaRange(%r)" % self.media_range - -def categoryMatches(this, that): - - """ - Return the basis of a match between 'this' and 'that' or False if the given - categories do not match. - """ - - return (this == "*" or this == that) and this or \ - that == "*" and that or False - -def categoryPermits(this, that): - - """ - Return whether 'this' category permits 'that' category. Where 'this' is a - wildcard ("*"), 'that' should always match. A value of False is returned if - the categories do not otherwise match. - """ - - return (this == "*" or this == that) and this or False - -def getMappingFromParameterStrings(l): - - """ - Return a mapping representing the list of "name=value" strings given by 'l'. - """ - - parameters = {} - - for parameter in l: - parts = parameter.split("=") - name = parts[0].strip() - value = "=".join(parts[1:]).strip() - parameters[name] = value - - return parameters - -def getContentPreferences(accept): - - """ - Return a mapping from media types to parameters for content/media types - extracted from the given 'accept' header value. The mapping is returned in - the form of a list of (media type, parameters) tuples. - - See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.1 - """ - - preferences = [] - - for field in accept.split(","): - - # The media type with parameters (defined by the "media-range") is - # separated from any other parameters (defined as "accept-extension" - # parameters) by a quality parameter. - - fparts = accept_regexp.split(field) - - # The first part is always the media type. - - media_type = fparts[0].strip() - - # Any other parts can be interpreted as extension parameters. - - if len(fparts) > 1: - fparts = ("q=" + ";q=".join(fparts[1:])).split(";") - else: - fparts = [] - - # Each field in the preferences can incorporate parameters separated by - # semicolon characters. - - parameters = getMappingFromParameterStrings(fparts) - media_range = MediaRange(media_type, parameters) - preferences.append(media_range) - - return ContentPreferences(preferences) - -class ContentPreferences: - - "A wrapper around content preference information." - - def __init__(self, preferences): - self.preferences = preferences - - def __iter__(self): - return iter(self.preferences) - - def get_ordered(self, by_quality=0): - - """ - Return a list of content/media types in descending order of preference. - If 'by_quality' is set to a true value, the "q" value will be used as - the primary measure of preference; otherwise, only the specificity will - be considered. - """ - - ordered = {} - - for media_range in self.preferences: - specificity = media_range.get_specificity() - - if by_quality: - q = float(media_range.accept_parameters.get("q", "1")) - key = q, specificity - else: - key = specificity - - if not ordered.has_key(key): - ordered[key] = [] - - ordered[key].append(media_range) - - # Return the preferences in descending order of quality and specificity. - - keys = ordered.keys() - keys.sort(reverse=True) - return [ordered[key] for key in keys] - - def get_acceptable_types(self, available): - - """ - Return content/media types from those in the 'available' list supported - by the known preferences grouped by preference level in descending order - of preference. - """ - - matches = {} - available = set(available[:]) - - for level in self.get_ordered(): - for media_range in level: - - # Attempt to match available types. - - found = set() - for available_type in available: - if media_range.permits(available_type): - q = float(media_range.accept_parameters.get("q", "1")) - if not matches.has_key(q): - matches[q] = [] - matches[q].append(available_type) - found.add(available_type) - - # Stop looking for matches for matched available types. - - if found: - available.difference_update(found) - - # Sort the matches in descending order of quality. - - all_q = matches.keys() - - if all_q: - all_q.sort(reverse=True) - return [matches[q] for q in all_q] - else: - return [] - - def get_preferred_types(self, available): - - """ - Return the preferred content/media types from those in the 'available' - list, given the known preferences. - """ - - preferred = self.get_acceptable_types(available) - if preferred: - return preferred[0] - else: - return [] - -# Content type parsing. - -def getContentTypeAndEncoding(content_type): - - """ - Return a tuple with the content/media type and encoding, extracted from the - given 'content_type' header value. - """ - - m = encoding_regexp.search(content_type) - if m: - return m.group("content_type"), m.group("encoding") - else: - return None, None - # Page access functions. def getPageURL(page): diff -r 4bb7e5737873 -r 367c9d96bc2a README.txt --- a/README.txt Fri Jan 04 22:57:15 2013 +0100 +++ b/README.txt Sat Jan 19 16:08:29 2013 +0100 @@ -84,6 +84,8 @@ * Added form field dictionary manipulation in the 1.9 compatibility class. * Added parameterisation of the formatText function so that paragraphs may be generated for formatted text. + * Added various content/media type functions from EventAggregator and other + projects to ContentTypeSupport. Release Procedures ------------------