Latest Version: 0.9.6.2
/Users/bbangert/Programming/Python/Paste/paste/wsgiwrappers.py
0001# (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
0002# Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
0003"""WSGI Wrappers for a Request and Response
0004
0005The WSGIRequest and WSGIResponse objects are light wrappers to make it easier
0006to deal with an incoming request and sending a response.
0007"""
0008import re
0009import warnings
0010from pprint import pformat
0011from Cookie import SimpleCookie
0012from paste.request import EnvironHeaders, get_cookie_dict,       parse_dict_querystring, parse_formvars
0014from paste.util.multidict import MultiDict, UnicodeMultiDict
0015from paste.registry import StackedObjectProxy
0016from paste.response import HeaderDict
0017from paste.wsgilib import encode_unicode_app_iter
0018from paste.httpheaders import ACCEPT_LANGUAGE
0019from paste.util.mimeparse import desired_matches
0020
0021__all__ = ['WSGIRequest', 'WSGIResponse']
0022
0023_CHARSET_RE = re.compile(r'.*;\s*charset=(.*?)(;|$)', re.I)
0024
0025class DeprecatedSettings(StackedObjectProxy):
0026    def _push_object(self, obj):
0027        warnings.warn('paste.wsgiwrappers.settings is deprecated: Please use '
0028                      'paste.wsgiwrappers.WSGIRequest.defaults instead',
0029                      DeprecationWarning, 3)
0030        WSGIResponse.defaults._push_object(obj)
0031        StackedObjectProxy._push_object(self, obj)
0032
0033# settings is deprecated: use WSGIResponse.defaults instead
0034settings = DeprecatedSettings(default=dict())
0035
0036class environ_getter(object):
0037    """For delegating an attribute to a key in self.environ."""
0038    # @@: Also __set__?  Should setting be allowed?
0039    def __init__(self, key, default='', default_factory=None):
0040        self.key = key
0041        self.default = default
0042        self.default_factory = default_factory
0043    def __get__(self, obj, type=None):
0044        if type is None:
0045            return self
0046        if self.key not in obj.environ:
0047            if self.default_factory:
0048                val = obj.environ[self.key] = self.default_factory()
0049                return val
0050            else:
0051                return self.default
0052        return obj.environ[self.key]
0053
0054    def __repr__(self):
0055        return '<Proxy for WSGI environ %r key>' % self.key
0056
0057class WSGIRequest(object):
0058    """WSGI Request API Object
0059
0060    This object represents a WSGI request with a more friendly interface.
0061    This does not expose every detail of the WSGI environment, and attempts
0062    to express nothing beyond what is available in the environment
0063    dictionary.
0064
0065    The only state maintained in this object is the desired ``charset``,
0066    its associated ``errors`` handler, and the ``decode_param_names``
0067    option.
0068
0069    The incoming parameter values will be automatically coerced to unicode
0070    objects of the ``charset`` encoding when ``charset`` is set. The
0071    incoming parameter names are not decoded to unicode unless the
0072    ``decode_param_names`` option is enabled.
0073
0074    When unicode is expected, ``charset`` will overridden by the the
0075    value of the ``Content-Type`` header's charset parameter if one was
0076    specified by the client.
0077
0078    The class variable ``defaults`` specifies default values for
0079    ``charset``, ``errors``, and ``langauge``. These can be overridden for the
0080    current request via the registry.
0081        
0082    The ``language`` default value is considered the fallback during i18n
0083    translations to ensure in odd cases that mixed languages don't occur should
0084    the ``language`` file contain the string but not another language in the
0085    accepted languages list. The ``language`` value only applies when getting
0086    a list of accepted languages from the HTTP Accept header.
0087    
0088    This behavior is duplicated from Aquarium, and may seem strange but is
0089    very useful. Normally, everything in the code is in "en-us".  However, 
0090    the "en-us" translation catalog is usually empty.  If the user requests
0091    ``["en-us", "zh-cn"]`` and a translation isn't found for a string in
0092    "en-us", you don't want gettext to fallback to "zh-cn".  You want it to 
0093    just use the string itself.  Hence, if a string isn't found in the
0094    ``language`` catalog, the string in the source code will be used.
0095
0096    *All* other state is kept in the environment dictionary; this is
0097    essential for interoperability.
0098
0099    You are free to subclass this object.
0100
0101    """
0102    defaults = StackedObjectProxy(default=dict(charset=None, errors='replace',
0103                                               decode_param_names=False,
0104                                               language='en-us'))
0105    def __init__(self, environ):
0106        self.environ = environ
0107        # This isn't "state" really, since the object is derivative:
0108        self.headers = EnvironHeaders(environ)
0109
0110        defaults = self.defaults._current_obj()
0111        self.charset = defaults.get('charset')
0112        if self.charset:
0113            # There's a charset: params will be coerced to unicode. In that
0114            # case, attempt to use the charset specified by the browser
0115            browser_charset = self.determine_browser_charset()
0116            if browser_charset:
0117                self.charset = browser_charset
0118        self.errors = defaults.get('errors', 'strict')
0119        self.decode_param_names = defaults.get('decode_param_names', False)
0120        self._languages = None
0121
0122    body = environ_getter('wsgi.input')
0123    scheme = environ_getter('wsgi.url_scheme')
0124    method = environ_getter('REQUEST_METHOD')
0125    script_name = environ_getter('SCRIPT_NAME')
0126    path_info = environ_getter('PATH_INFO')
0127
0128    def urlvars(self):
0129        """
0130        Return any variables matched in the URL (e.g.,
0131        ``wsgiorg.routing_args``).
0132        """
0133        if 'paste.urlvars' in self.environ:
0134            return self.environ['paste.urlvars']
0135        elif 'wsgiorg.routing_args' in self.environ:
0136            return self.environ['wsgiorg.routing_args'][1]
0137        else:
0138            return {}
0139    urlvars = property(urlvars, doc=urlvars.__doc__)
0140
0141    def is_xhr(self):
0142        """Returns a boolean if X-Requested-With is present and a XMLHttpRequest"""
0143        return self.environ.get('HTTP_X_REQUESTED_WITH', '') == 'XMLHttpRequest'
0144    is_xhr = property(is_xhr, doc=is_xhr.__doc__)
0145
0146    def host(self):
0147        """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME"""
0148        return self.environ.get('HTTP_HOST', self.environ.get('SERVER_NAME'))
0149    host = property(host, doc=host.__doc__)
0150
0151    def languages(self):
0152        """Return a list of preferred languages, most preferred first.
0153        
0154        The list may be empty.
0155        """
0156        if self._languages is not None:
0157            return self._languages
0158        acceptLanguage = self.environ.get('HTTP_ACCEPT_LANGUAGE')
0159        langs = ACCEPT_LANGUAGE.parse(self.environ)
0160        fallback = self.defaults.get('language', 'en-us')
0161        if not fallback:
0162            return langs
0163        if fallback not in langs:
0164            langs.append(fallback)
0165        index = langs.index(fallback)
0166        langs[index+1:] = []
0167        self._languages = langs
0168        return self._languages
0169    languages = property(languages, doc=languages.__doc__)
0170
0171    def _GET(self):
0172        return parse_dict_querystring(self.environ)
0173
0174    def GET(self):
0175        """
0176        Dictionary-like object representing the QUERY_STRING
0177        parameters. Always present, if possibly empty.
0178
0179        If the same key is present in the query string multiple times, a
0180        list of its values can be retrieved from the ``MultiDict`` via
0181        the ``getall`` method.
0182
0183        Returns a ``MultiDict`` container or a ``UnicodeMultiDict`` when
0184        ``charset`` is set.
0185        """
0186        params = self._GET()
0187        if self.charset:
0188            params = UnicodeMultiDict(params, encoding=self.charset,
0189                                      errors=self.errors,
0190                                      decode_keys=self.decode_param_names)
0191        return params
0192    GET = property(GET, doc=GET.__doc__)
0193
0194    def _POST(self):
0195        return parse_formvars(self.environ, include_get_vars=False)
0196
0197    def POST(self):
0198        """Dictionary-like object representing the POST body.
0199
0200        Most values are encoded strings, or unicode strings when
0201        ``charset`` is set. There may also be FieldStorage objects
0202        representing file uploads. If this is not a POST request, or the
0203        body is not encoded fields (e.g., an XMLRPC request) then this
0204        will be empty.
0205
0206        This will consume wsgi.input when first accessed if applicable,
0207        but the raw version will be put in
0208        environ['paste.parsed_formvars'].
0209
0210        Returns a ``MultiDict`` container or a ``UnicodeMultiDict`` when
0211        ``charset`` is set.
0212        """
0213        params = self._POST()
0214        if self.charset:
0215            params = UnicodeMultiDict(params, encoding=self.charset,
0216                                      errors=self.errors,
0217                                      decode_keys=self.decode_param_names)
0218        return params
0219    POST = property(POST, doc=POST.__doc__)
0220
0221    def params(self):
0222        """Dictionary-like object of keys from POST, GET, URL dicts
0223
0224        Return a key value from the parameters, they are checked in the
0225        following order: POST, GET, URL
0226
0227        Additional methods supported:
0228
0229        ``getlist(key)``
0230            Returns a list of all the values by that key, collected from
0231            POST, GET, URL dicts
0232
0233        Returns a ``MultiDict`` container or a ``UnicodeMultiDict`` when
0234        ``charset`` is set.
0235        """
0236        params = MultiDict()
0237        params.update(self._POST())
0238        params.update(self._GET())
0239        if self.charset:
0240            params = UnicodeMultiDict(params, encoding=self.charset,
0241                                      errors=self.errors,
0242                                      decode_keys=self.decode_param_names)
0243        return params
0244    params = property(params, doc=params.__doc__)
0245
0246    def cookies(self):
0247        """Dictionary of cookies keyed by cookie name.
0248
0249        Just a plain dictionary, may be empty but not None.
0250        
0251        """
0252        return get_cookie_dict(self.environ)
0253    cookies = property(cookies, doc=cookies.__doc__)
0254
0255    def determine_browser_charset(self):
0256        """
0257        Determine the encoding as specified by the browser via the
0258        Content-Type's charset parameter, if one is set
0259        """
0260        charset_match = _CHARSET_RE.match(self.headers.get('Content-Type', ''))
0261        if charset_match:
0262            return charset_match.group(1)
0263
0264    def match_accept(self, mimetypes):
0265        """Return a list of specified mime-types that the browser's HTTP Accept
0266        header allows in the order provided."""
0267        return desired_matches(mimetypes,
0268                               self.environ.get('HTTP_ACCEPT', '*/*'))
0269
0270    def __repr__(self):
0271        """Show important attributes of the WSGIRequest"""
0272        pf = pformat
0273        msg = '<%s.%s object at 0x%x method=%s,' %               (self.__class__.__module__, self.__class__.__name__,
0275             id(self), pf(self.method))
0276        msg += '\nscheme=%s, host=%s, script_name=%s, path_info=%s,' %               (pf(self.scheme), pf(self.host), pf(self.script_name),
0278             pf(self.path_info))
0279        msg += '\nlanguges=%s,' % pf(self.languages)
0280        if self.charset:
0281            msg += ' charset=%s, errors=%s,' % (pf(self.charset),
0282                                                pf(self.errors))
0283        msg += '\nGET=%s,' % pf(self.GET)
0284        msg += '\nPOST=%s,' % pf(self.POST)
0285        msg += '\ncookies=%s>' % pf(self.cookies)
0286        return msg
0287
0288class WSGIResponse(object):
0289    """A basic HTTP response with content, headers, and out-bound cookies
0290
0291    The class variable ``defaults`` specifies default values for
0292    ``content_type``, ``charset`` and ``errors``. These can be overridden
0293    for the current request via the registry.
0294
0295    """
0296    defaults = StackedObjectProxy(
0297        default=dict(content_type='text/html', charset='utf-8',
0298                     errors='strict', headers={'Cache-Control':'no-cache'})
0299        )
0300    def __init__(self, content='', mimetype=None, code=200):
0301        self._iter = None
0302        self._is_str_iter = True
0303
0304        self.content = content
0305        self.headers = HeaderDict()
0306        self.cookies = SimpleCookie()
0307        self.status_code = code
0308
0309        defaults = self.defaults._current_obj()
0310        if not mimetype:
0311            mimetype = defaults.get('content_type', 'text/html')
0312            charset = defaults.get('charset')
0313            if charset:
0314                mimetype = '%s; charset=%s' % (mimetype, charset)
0315        self.headers.update(defaults.get('headers', {}))
0316        self.headers['Content-Type'] = mimetype
0317        self.errors = defaults.get('errors', 'strict')
0318
0319    def __str__(self):
0320        """Returns a rendition of the full HTTP message, including headers.
0321
0322        When the content is an iterator, the actual content is replaced with the
0323        output of str(iterator) (to avoid exhausting the iterator).
0324        """
0325        if self._is_str_iter:
0326            content = ''.join(self.get_content())
0327        else:
0328            content = str(self.content)
0329        return '\n'.join(['%s: %s' % (key, value)
0330            for key, value in self.headers.headeritems()])               + '\n\n' + content
0332
0333    def __call__(self, environ, start_response):
0334        """Convenience call to return output and set status information
0335        
0336        Conforms to the WSGI interface for calling purposes only.
0337        
0338        Example usage:
0339        
0340        .. code-block:: Python
0341
0342            def wsgi_app(environ, start_response):
0343                response = WSGIResponse()
0344                response.write("Hello world")
0345                response.headers['Content-Type'] = 'latin1'
0346                return response(environ, start_response)
0347        
0348        """
0349        status_text = STATUS_CODE_TEXT[self.status_code]
0350        status = '%s %s' % (self.status_code, status_text)
0351        response_headers = self.headers.headeritems()
0352        for c in self.cookies.values():
0353            response_headers.append(('Set-Cookie', c.output(header='')))
0354        start_response(status, response_headers)
0355        is_file = isinstance(self.content, file)
0356        if 'wsgi.file_wrapper' in environ and is_file:
0357            return environ['wsgi.file_wrapper'](self.content)
0358        elif is_file:
0359            return iter(lambda: self.content.read(), '')
0360        return self.get_content()
0361
0362    def determine_charset(self):
0363        """
0364        Determine the encoding as specified by the Content-Type's charset
0365        parameter, if one is set
0366        """
0367        charset_match = _CHARSET_RE.match(self.headers.get('Content-Type', ''))
0368        if charset_match:
0369            return charset_match.group(1)
0370
0371    def has_header(self, header):
0372        """
0373        Case-insensitive check for a header
0374        """
0375        warnings.warn('WSGIResponse.has_header is deprecated, use '
0376                      'WSGIResponse.headers.has_key instead', DeprecationWarning,
0377                      2)
0378        return self.headers.has_key(header)
0379
0380    def set_cookie(self, key, value='', max_age=None, expires=None, path='/',
0381                   domain=None, secure=None):
0382        """
0383        Define a cookie to be sent via the outgoing HTTP headers
0384        """
0385        self.cookies[key] = value
0386        for var_name, var_value in [
0387            ('max_age', max_age), ('path', path), ('domain', domain),
0388            ('secure', secure), ('expires', expires)]:
0389            if var_value is not None and var_value is not False:
0390                self.cookies[key][var_name.replace('_', '-')] = var_value
0391
0392    def delete_cookie(self, key, path='/', domain=None):
0393        """
0394        Notify the browser the specified cookie has expired and should be
0395        deleted (via the outgoing HTTP headers)
0396        """
0397        self.cookies[key] = ''
0398        if path is not None:
0399            self.cookies[key]['path'] = path
0400        if domain is not None:
0401            self.cookies[key]['domain'] = path
0402        self.cookies[key]['expires'] = 0
0403        self.cookies[key]['max-age'] = 0
0404
0405    def _set_content(self, content):
0406        if hasattr(content, '__iter__'):
0407            self._iter = content
0408            if isinstance(content, list):
0409                self._is_str_iter = True
0410            else:
0411                self._is_str_iter = False
0412        else:
0413            self._iter = [content]
0414            self._is_str_iter = True
0415    content = property(lambda self: self._iter, _set_content,
0416                       doc='Get/set the specified content, where content can '
0417                       'be: a string, a list of strings, a generator function '
0418                       'that yields strings, or an iterable object that '
0419                       'produces strings.')
0420
0421    def get_content(self):
0422        """
0423        Returns the content as an iterable of strings, encoding each element of
0424        the iterator from a Unicode object if necessary.
0425        """
0426        charset = self.determine_charset()
0427        if charset:
0428            return encode_unicode_app_iter(self.content, charset, self.errors)
0429        else:
0430            return self.content
0431
0432    def wsgi_response(self):
0433        """
0434        Return this WSGIResponse as a tuple of WSGI formatted data, including:
0435        (status, headers, iterable)
0436        """
0437        status_text = STATUS_CODE_TEXT[self.status_code]
0438        status = '%s %s' % (self.status_code, status_text)
0439        response_headers = self.headers.headeritems()
0440        for c in self.cookies.values():
0441            response_headers.append(('Set-Cookie', c.output(header='')))
0442        return status, response_headers, self.get_content()
0443
0444    # The remaining methods partially implement the file-like object interface.
0445    # See http://docs.python.org/lib/bltin-file-objects.html
0446    def write(self, content):
0447        if not self._is_str_iter:
0448            raise IOError, "This %s instance's content is not writable: (content "                   'is an iterator)' % self.__class__.__name__
0450        self.content.append(content)
0451
0452    def flush(self):
0453        pass
0454
0455    def tell(self):
0456        if not self._is_str_iter:
0457            raise IOError, 'This %s instance cannot tell its position: (content '                   'is an iterator)' % self.__class__.__name__
0459        return sum([len(chunk) for chunk in self._iter])
0460
0461    ########################################
0462    ## Content-type and charset
0463
0464    def charset__get(self):
0465        """
0466