Home | History | Annotate | Download | only in paste
      1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
      2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
      3 """Routines to generate WSGI responses"""
      4 
      5 ############################################################
      6 ## Headers
      7 ############################################################
      8 import warnings
      9 
     10 class HeaderDict(dict):
     11 
     12     """
     13     This represents response headers.  It handles the headers as a
     14     dictionary, with case-insensitive keys.
     15 
     16     Also there is an ``.add(key, value)`` method, which sets the key,
     17     or adds the value to the current value (turning it into a list if
     18     necessary).
     19 
     20     For passing to WSGI there is a ``.headeritems()`` method which is
     21     like ``.items()`` but unpacks value that are lists.  It also
     22     handles encoding -- all headers are encoded in ASCII (if they are
     23     unicode).
     24 
     25     @@: Should that encoding be ISO-8859-1 or UTF-8?  I'm not sure
     26     what the spec says.
     27     """
     28 
     29     def __getitem__(self, key):
     30         return dict.__getitem__(self, self.normalize(key))
     31 
     32     def __setitem__(self, key, value):
     33         dict.__setitem__(self, self.normalize(key), value)
     34 
     35     def __delitem__(self, key):
     36         dict.__delitem__(self, self.normalize(key))
     37 
     38     def __contains__(self, key):
     39         return dict.__contains__(self, self.normalize(key))
     40 
     41     has_key = __contains__
     42 
     43     def get(self, key, failobj=None):
     44         return dict.get(self, self.normalize(key), failobj)
     45 
     46     def setdefault(self, key, failobj=None):
     47         return dict.setdefault(self, self.normalize(key), failobj)
     48 
     49     def pop(self, key, *args):
     50         return dict.pop(self, self.normalize(key), *args)
     51 
     52     def update(self, other):
     53         for key in other:
     54             self[self.normalize(key)] = other[key]
     55 
     56     def normalize(self, key):
     57         return str(key).lower().strip()
     58 
     59     def add(self, key, value):
     60         key = self.normalize(key)
     61         if key in self:
     62             if isinstance(self[key], list):
     63                 self[key].append(value)
     64             else:
     65                 self[key] = [self[key], value]
     66         else:
     67             self[key] = value
     68 
     69     def headeritems(self):
     70         result = []
     71         for key, value in self.items():
     72             if isinstance(value, list):
     73                 for v in value:
     74                     result.append((key, str(v)))
     75             else:
     76                 result.append((key, str(value)))
     77         return result
     78 
     79     #@classmethod
     80     def fromlist(cls, seq):
     81         self = cls()
     82         for name, value in seq:
     83             self.add(name, value)
     84         return self
     85 
     86     fromlist = classmethod(fromlist)
     87 
     88 def has_header(headers, name):
     89     """
     90     Is header named ``name`` present in headers?
     91     """
     92     name = name.lower()
     93     for header, value in headers:
     94         if header.lower() == name:
     95             return True
     96     return False
     97 
     98 def header_value(headers, name):
     99     """
    100     Returns the header's value, or None if no such header.  If a
    101     header appears more than once, all the values of the headers
    102     are joined with ','.   Note that this is consistent /w RFC 2616
    103     section 4.2 which states:
    104 
    105         It MUST be possible to combine the multiple header fields
    106         into one "field-name: field-value" pair, without changing
    107         the semantics of the message, by appending each subsequent
    108         field-value to the first, each separated by a comma.
    109 
    110     However, note that the original netscape usage of 'Set-Cookie',
    111     especially in MSIE which contains an 'expires' date will is not
    112     compatible with this particular concatination method.
    113     """
    114     name = name.lower()
    115     result = [value for header, value in headers
    116               if header.lower() == name]
    117     if result:
    118         return ','.join(result)
    119     else:
    120         return None
    121 
    122 def remove_header(headers, name):
    123     """
    124     Removes the named header from the list of headers.  Returns the
    125     value of that header, or None if no header found.  If multiple
    126     headers are found, only the last one is returned.
    127     """
    128     name = name.lower()
    129     i = 0
    130     result = None
    131     while i < len(headers):
    132         if headers[i][0].lower() == name:
    133             result = headers[i][1]
    134             del headers[i]
    135             continue
    136         i += 1
    137     return result
    138 
    139 def replace_header(headers, name, value):
    140     """
    141     Updates the headers replacing the first occurance of the given name
    142     with the value provided; asserting that no further occurances
    143     happen. Note that this is _not_ the same as remove_header and then
    144     append, as two distinct operations (del followed by an append) are
    145     not atomic in a threaded environment. Returns the previous header
    146     value for the provided name, if any.   Clearly one should not use
    147     this function with ``set-cookie`` or other names that may have more
    148     than one occurance in the headers.
    149     """
    150     name = name.lower()
    151     i = 0
    152     result = None
    153     while i < len(headers):
    154         if headers[i][0].lower() == name:
    155             assert not result, "two values for the header '%s' found" % name
    156             result = headers[i][1]
    157             headers[i] = (name, value)
    158         i += 1
    159     if not result:
    160         headers.append((name, value))
    161     return result
    162 
    163 
    164 ############################################################
    165 ## Deprecated methods
    166 ############################################################
    167 
    168 def error_body_response(error_code, message, __warn=True):
    169     """
    170     Returns a standard HTML response page for an HTTP error.
    171     **Note:** Deprecated
    172     """
    173     if __warn:
    174         warnings.warn(
    175             'wsgilib.error_body_response is deprecated; use the '
    176             'wsgi_application method on an HTTPException object '
    177             'instead', DeprecationWarning, 2)
    178     return '''\
    179 <html>
    180   <head>
    181     <title>%(error_code)s</title>
    182   </head>
    183   <body>
    184   <h1>%(error_code)s</h1>
    185   %(message)s
    186   </body>
    187 </html>''' % {
    188         'error_code': error_code,
    189         'message': message,
    190         }
    191 
    192 
    193 def error_response(environ, error_code, message,
    194                    debug_message=None, __warn=True):
    195     """
    196     Returns the status, headers, and body of an error response.
    197 
    198     Use like:
    199 
    200     .. code-block:: python
    201 
    202         status, headers, body = wsgilib.error_response(
    203             '301 Moved Permanently', 'Moved to <a href="%s">%s</a>'
    204             % (url, url))
    205         start_response(status, headers)
    206         return [body]
    207 
    208     **Note:** Deprecated
    209     """
    210     if __warn:
    211         warnings.warn(
    212             'wsgilib.error_response is deprecated; use the '
    213             'wsgi_application method on an HTTPException object '
    214             'instead', DeprecationWarning, 2)
    215     if debug_message and environ.get('paste.config', {}).get('debug'):
    216         message += '\n\n<!-- %s -->' % debug_message
    217     body = error_body_response(error_code, message, __warn=False)
    218     headers = [('content-type', 'text/html'),
    219                ('content-length', str(len(body)))]
    220     return error_code, headers, body
    221 
    222 def error_response_app(error_code, message, debug_message=None,
    223                        __warn=True):
    224     """
    225     An application that emits the given error response.
    226 
    227     **Note:** Deprecated
    228     """
    229     if __warn:
    230         warnings.warn(
    231             'wsgilib.error_response_app is deprecated; use the '
    232             'wsgi_application method on an HTTPException object '
    233             'instead', DeprecationWarning, 2)
    234     def application(environ, start_response):
    235         status, headers, body = error_response(
    236             environ, error_code, message,
    237             debug_message=debug_message, __warn=False)
    238         start_response(status, headers)
    239         return [body]
    240     return application
    241