WebOb-1.3.1/0000775000175000017500000000000012252637046013317 5ustar chrismchrism00000000000000WebOb-1.3.1/WebOb.egg-info/0000775000175000017500000000000012252637046016007 5ustar chrismchrism00000000000000WebOb-1.3.1/WebOb.egg-info/PKG-INFO0000644000175000017500000000325512252637045017106 0ustar chrismchrism00000000000000Metadata-Version: 1.1 Name: WebOb Version: 1.3.1 Summary: WSGI request and response object Home-page: http://webob.org/ Author: Pylons Project Author-email: ianb@colorstudy.com License: MIT Description: WebOb provides wrappers around the WSGI request environment, and an object to help create WSGI responses. The objects map much of the specified behavior of HTTP, including header parsing and accessors for other standard parts of the environment. You may install the `in-development version of WebOb `_ with ``pip install WebOb==dev`` (or ``easy_install WebOb==dev``). * `WebOb reference `_ * `Bug tracker `_ * `Browse source code `_ * `Mailing list `_ * `Release news `_ * `Detailed changelog `_ Keywords: wsgi request web http Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 WebOb-1.3.1/WebOb.egg-info/zip-safe0000644000175000017500000000000111635437360017436 0ustar chrismchrism00000000000000 WebOb-1.3.1/WebOb.egg-info/dependency_links.txt0000644000175000017500000000000112252637045022052 0ustar chrismchrism00000000000000 WebOb-1.3.1/WebOb.egg-info/top_level.txt0000644000175000017500000000000612252637045020532 0ustar chrismchrism00000000000000webob WebOb-1.3.1/WebOb.egg-info/SOURCES.txt0000644000175000017500000000350212252637046017671 0ustar chrismchrism00000000000000.gitignore MANIFEST.in setup.cfg setup.py tox.ini toxfast.ini WebOb.egg-info/PKG-INFO WebOb.egg-info/SOURCES.txt WebOb.egg-info/dependency_links.txt WebOb.egg-info/requires.txt WebOb.egg-info/top_level.txt WebOb.egg-info/zip-safe docs/comment-example.txt docs/conf.py docs/differences.txt docs/do-it-yourself.txt docs/doctests.py docs/file-example.txt docs/index.txt docs/jsonrpc-example.txt docs/license.txt docs/news.txt docs/reference.txt docs/test-file.txt docs/test_dec.txt docs/test_request.txt docs/test_response.txt docs/todo.txt docs/wiki-example.txt docs/comment-example-code/example.py docs/jsonrpc-example-code/jsonrpc.py docs/jsonrpc-example-code/test_jsonrpc.py docs/jsonrpc-example-code/test_jsonrpc.txt docs/modules/client.txt docs/modules/dec.txt docs/modules/static.txt docs/modules/webob.txt docs/pycon2011/pycon-py3k-sprint.txt docs/pycon2011/request_table.rst docs/pycon2011/response_table.rst docs/wiki-example-code/example.py tests/__init__.py tests/conftest.py tests/performance_test.py tests/test_acceptparse.py tests/test_byterange.py tests/test_cachecontrol.py tests/test_client.py tests/test_client_functional.py tests/test_compat.py tests/test_cookies.py tests/test_datetime_utils.py tests/test_dec.py tests/test_descriptors.py tests/test_etag.py tests/test_etag_nose.py tests/test_exc.py tests/test_headers.py tests/test_in_wsgiref.py tests/test_misc.py tests/test_multidict.py tests/test_request.py tests/test_request_nose.py tests/test_response.py tests/test_static.py tests/test_transcode.py tests/test_util.py webob/__init__.py webob/acceptparse.py webob/byterange.py webob/cachecontrol.py webob/client.py webob/compat.py webob/cookies.py webob/datetime_utils.py webob/dec.py webob/descriptors.py webob/etag.py webob/exc.py webob/headers.py webob/multidict.py webob/request.py webob/response.py webob/static.py webob/util.pyWebOb-1.3.1/WebOb.egg-info/requires.txt0000664000175000017500000000005012252637045020401 0ustar chrismchrism00000000000000 [docs] Sphinx [testing] nose coverageWebOb-1.3.1/webob/0000775000175000017500000000000012252637046014415 5ustar chrismchrism00000000000000WebOb-1.3.1/webob/descriptors.py0000664000175000017500000002147312123261311017320 0ustar chrismchrism00000000000000from datetime import ( date, datetime, ) import re from webob.byterange import ( ContentRange, Range, ) from webob.compat import ( PY3, text_type, ) from webob.datetime_utils import ( parse_date, serialize_date, ) from webob.util import ( header_docstring, warn_deprecation, ) CHARSET_RE = re.compile(r';\s*charset=([^;]*)', re.I) SCHEME_RE = re.compile(r'^[a-z]+:', re.I) _not_given = object() def environ_getter(key, default=_not_given, rfc_section=None): if rfc_section: doc = header_docstring(key, rfc_section) else: doc = "Gets and sets the ``%s`` key in the environment." % key if default is _not_given: def fget(req): return req.environ[key] def fset(req, val): req.environ[key] = val fdel = None else: def fget(req): return req.environ.get(key, default) def fset(req, val): if val is None: if key in req.environ: del req.environ[key] else: req.environ[key] = val def fdel(req): del req.environ[key] return property(fget, fset, fdel, doc=doc) def environ_decoder(key, default=_not_given, rfc_section=None, encattr=None): if rfc_section: doc = header_docstring(key, rfc_section) else: doc = "Gets and sets the ``%s`` key in the environment." % key if default is _not_given: def fget(req): return req.encget(key, encattr=encattr) def fset(req, val): return req.encset(key, val, encattr=encattr) fdel = None else: def fget(req): return req.encget(key, default, encattr=encattr) def fset(req, val): if val is None: if key in req.environ: del req.environ[key] else: return req.encset(key, val, encattr=encattr) def fdel(req): del req.environ[key] return property(fget, fset, fdel, doc=doc) def upath_property(key): if PY3: # pragma: no cover def fget(req): encoding = req.url_encoding return req.environ.get(key, '').encode('latin-1').decode(encoding) def fset(req, val): encoding = req.url_encoding req.environ[key] = val.encode(encoding).decode('latin-1') else: def fget(req): encoding = req.url_encoding return req.environ.get(key, '').decode(encoding) def fset(req, val): encoding = req.url_encoding if isinstance(val, text_type): val = val.encode(encoding) req.environ[key] = val return property(fget, fset, doc='upath_property(%r)' % key) def deprecated_property(attr, name, text, version): # pragma: no cover """ Wraps a descriptor, with a deprecation warning or error """ def warn(): warn_deprecation('The attribute %s is deprecated: %s' % (attr, text), version, 3 ) def fget(self): warn() return attr.__get__(self, type(self)) def fset(self, val): warn() attr.__set__(self, val) def fdel(self): warn() attr.__delete__(self) return property(fget, fset, fdel, '' % attr ) def header_getter(header, rfc_section): doc = header_docstring(header, rfc_section) key = header.lower() def fget(r): for k, v in r._headerlist: if k.lower() == key: return v def fset(r, value): fdel(r) if value is not None: if isinstance(value, text_type) and not PY3: value = value.encode('latin-1') r._headerlist.append((header, value)) def fdel(r): items = r._headerlist for i in range(len(items)-1, -1, -1): if items[i][0].lower() == key: del items[i] return property(fget, fset, fdel, doc) def converter(prop, parse, serialize, convert_name=None): assert isinstance(prop, property) convert_name = convert_name or "``%s`` and ``%s``" % (parse.__name__, serialize.__name__) doc = prop.__doc__ or '' doc += " Converts it using %s." % convert_name hget, hset = prop.fget, prop.fset def fget(r): return parse(hget(r)) def fset(r, val): if val is not None: val = serialize(val) hset(r, val) return property(fget, fset, prop.fdel, doc) def list_header(header, rfc_section): prop = header_getter(header, rfc_section) return converter(prop, parse_list, serialize_list, 'list') def parse_list(value): if not value: return None return tuple(filter(None, [v.strip() for v in value.split(',')])) def serialize_list(value): if isinstance(value, (text_type, bytes)): return str(value) else: return ', '.join(map(str, value)) def converter_date(prop): return converter(prop, parse_date, serialize_date, 'HTTP date') def date_header(header, rfc_section): return converter_date(header_getter(header, rfc_section)) ######################## ## Converter functions ######################## _rx_etag = re.compile(r'(?:^|\s)(W/)?"((?:\\"|.)*?)"') def parse_etag_response(value, strong=False): """ Parse a response ETag. See: * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.19 * http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11 """ if not value: return None m = _rx_etag.match(value) if not m: # this etag is invalid, but we'll just return it anyway return value elif strong and m.group(1): # this is a weak etag and we want only strong ones return None else: return m.group(2).replace('\\"', '"') def serialize_etag_response(value): #return '"%s"' % value.replace('"', '\\"') strong = True if isinstance(value, tuple): value, strong = value elif _rx_etag.match(value): # this is a valid etag already return value # let's quote the value r = '"%s"' % value.replace('"', '\\"') if not strong: r = 'W/' + r return r def serialize_if_range(value): if isinstance(value, (datetime, date)): return serialize_date(value) value = str(value) return value or None def parse_range(value): if not value: return None # Might return None too: return Range.parse(value) def serialize_range(value): if not value: return None elif isinstance(value, (list, tuple)): return str(Range(*value)) else: assert isinstance(value, str) return value def parse_int(value): if value is None or value == '': return None return int(value) def parse_int_safe(value): if value is None or value == '': return None try: return int(value) except ValueError: return None serialize_int = str def parse_content_range(value): if not value or not value.strip(): return None # May still return None return ContentRange.parse(value) def serialize_content_range(value): if isinstance(value, (tuple, list)): if len(value) not in (2, 3): raise ValueError( "When setting content_range to a list/tuple, it must " "be length 2 or 3 (not %r)" % value) if len(value) == 2: begin, end = value length = None else: begin, end, length = value value = ContentRange(begin, end, length) value = str(value).strip() if not value: return None return value _rx_auth_param = re.compile(r'([a-z]+)=(".*?"|[^,]*)(?:\Z|, *)') def parse_auth_params(params): r = {} for k, v in _rx_auth_param.findall(params): r[k] = v.strip('"') return r # see http://lists.w3.org/Archives/Public/ietf-http-wg/2009OctDec/0297.html known_auth_schemes = ['Basic', 'Digest', 'WSSE', 'HMACDigest', 'GoogleLogin', 'Cookie', 'OpenID'] known_auth_schemes = dict.fromkeys(known_auth_schemes, None) def parse_auth(val): if val is not None: authtype, params = val.split(' ', 1) if authtype in known_auth_schemes: if authtype == 'Basic' and '"' not in params: # this is the "Authentication: Basic XXXXX==" case pass else: params = parse_auth_params(params) return authtype, params return val def serialize_auth(val): if isinstance(val, (tuple, list)): authtype, params = val if isinstance(params, dict): params = ', '.join(map('%s="%s"'.__mod__, params.items())) assert isinstance(params, str) return '%s %s' % (authtype, params) return val WebOb-1.3.1/webob/exc.py0000664000175000017500000010637712127135226015556 0ustar chrismchrism00000000000000""" HTTP Exception -------------- This module processes Python exceptions that relate to HTTP exceptions by defining a set of exceptions, all subclasses of HTTPException. Each exception, in addition to being a Python exception that can be raised and caught, is also a WSGI application and ``webob.Response`` object. This module defines exceptions according to RFC 2068 [1]_ : codes with 100-300 are not really errors; 400's are client errors, and 500's are server errors. According to the WSGI specification [2]_ , the application can call ``start_response`` more then once only under two conditions: (a) the response has not yet been sent, or (b) if the second and subsequent invocations of ``start_response`` have a valid ``exc_info`` argument obtained from ``sys.exc_info()``. The WSGI specification then requires the server or gateway to handle the case where content has been sent and then an exception was encountered. Exception HTTPException HTTPOk * 200 - HTTPOk * 201 - HTTPCreated * 202 - HTTPAccepted * 203 - HTTPNonAuthoritativeInformation * 204 - HTTPNoContent * 205 - HTTPResetContent * 206 - HTTPPartialContent HTTPRedirection * 300 - HTTPMultipleChoices * 301 - HTTPMovedPermanently * 302 - HTTPFound * 303 - HTTPSeeOther * 304 - HTTPNotModified * 305 - HTTPUseProxy * 306 - Unused (not implemented, obviously) * 307 - HTTPTemporaryRedirect HTTPError HTTPClientError * 400 - HTTPBadRequest * 401 - HTTPUnauthorized * 402 - HTTPPaymentRequired * 403 - HTTPForbidden * 404 - HTTPNotFound * 405 - HTTPMethodNotAllowed * 406 - HTTPNotAcceptable * 407 - HTTPProxyAuthenticationRequired * 408 - HTTPRequestTimeout * 409 - HTTPConflict * 410 - HTTPGone * 411 - HTTPLengthRequired * 412 - HTTPPreconditionFailed * 413 - HTTPRequestEntityTooLarge * 414 - HTTPRequestURITooLong * 415 - HTTPUnsupportedMediaType * 416 - HTTPRequestRangeNotSatisfiable * 417 - HTTPExpectationFailed * 428 - HTTPPreconditionRequired * 429 - HTTPTooManyRequests * 431 - HTTPRequestHeaderFieldsTooLarge HTTPServerError * 500 - HTTPInternalServerError * 501 - HTTPNotImplemented * 502 - HTTPBadGateway * 503 - HTTPServiceUnavailable * 504 - HTTPGatewayTimeout * 505 - HTTPVersionNotSupported * 511 - HTTPNetworkAuthenticationRequired Subclass usage notes: --------------------- The HTTPException class is complicated by 4 factors: 1. The content given to the exception may either be plain-text or as html-text. 2. The template may want to have string-substitutions taken from the current ``environ`` or values from incoming headers. This is especially troublesome due to case sensitivity. 3. The final output may either be text/plain or text/html mime-type as requested by the client application. 4. Each exception has a default explanation, but those who raise exceptions may want to provide additional detail. Subclass attributes and call parameters are designed to provide an easier path through the complications. Attributes: ``code`` the HTTP status code for the exception ``title`` remainder of the status line (stuff after the code) ``explanation`` a plain-text explanation of the error message that is not subject to environment or header substitutions; it is accessible in the template via %(explanation)s ``detail`` a plain-text message customization that is not subject to environment or header substitutions; accessible in the template via %(detail)s ``body_template`` a content fragment (in HTML) used for environment and header substitution; the default template includes both the explanation and further detail provided in the message Parameters: ``detail`` a plain-text override of the default ``detail`` ``headers`` a list of (k,v) header pairs ``comment`` a plain-text additional information which is usually stripped/hidden for end-users ``body_template`` a string.Template object containing a content fragment in HTML that frames the explanation and further detail To override the template (which is HTML content) or the plain-text explanation, one must subclass the given exception; or customize it after it has been created. This particular breakdown of a message into explanation, detail and template allows both the creation of plain-text and html messages for various clients as well as error-free substitution of environment variables and headers. The subclasses of :class:`~_HTTPMove` (:class:`~HTTPMultipleChoices`, :class:`~HTTPMovedPermanently`, :class:`~HTTPFound`, :class:`~HTTPSeeOther`, :class:`~HTTPUseProxy` and :class:`~HTTPTemporaryRedirect`) are redirections that require a ``Location`` field. Reflecting this, these subclasses have two additional keyword arguments: ``location`` and ``add_slash``. Parameters: ``location`` to set the location immediately ``add_slash`` set to True to redirect to the same URL as the request, except with a ``/`` appended Relative URLs in the location will be resolved to absolute. References: .. [1] http://www.python.org/peps/pep-0333.html#error-handling .. [2] http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.5 """ from string import Template import re import sys from webob.compat import ( class_types, text_, text_type, urlparse, ) from webob.request import Request from webob.response import Response from webob.util import ( html_escape, warn_deprecation, ) tag_re = re.compile(r'<.*?>', re.S) br_re = re.compile(r'', re.I|re.S) comment_re = re.compile(r'') def no_escape(value): if value is None: return '' if not isinstance(value, text_type): if hasattr(value, '__unicode__'): value = value.__unicode__() if isinstance(value, bytes): value = text_(value, 'utf-8') else: value = text_type(value) return value def strip_tags(value): value = value.replace('\n', ' ') value = value.replace('\r', '') value = br_re.sub('\n', value) value = comment_re.sub('', value) value = tag_re.sub('', value) return value class HTTPException(Exception): def __init__(self, message, wsgi_response): Exception.__init__(self, message) self.wsgi_response = wsgi_response def __call__(self, environ, start_response): return self.wsgi_response(environ, start_response) # TODO: remove in version 1.3 @property def exception(self): warn_deprecation( "As of WebOb 1.2, raise the HTTPException instance directly " "instead of raising the result of 'HTTPException.exception'", '1.3', 2) return self class WSGIHTTPException(Response, HTTPException): ## You should set in subclasses: # code = 200 # title = 'OK' # explanation = 'why this happens' # body_template_obj = Template('response template') code = None title = None explanation = '' body_template_obj = Template('''\ ${explanation}

${detail} ${html_comment} ''') plain_template_obj = Template('''\ ${status} ${body}''') html_template_obj = Template('''\ ${status}

${status}

${body} ''') ## Set this to True for responses that should have no request body empty_body = False def __init__(self, detail=None, headers=None, comment=None, body_template=None, **kw): Response.__init__(self, status='%s %s' % (self.code, self.title), **kw) Exception.__init__(self, detail) if headers: self.headers.extend(headers) self.detail = detail self.comment = comment if body_template is not None: self.body_template = body_template self.body_template_obj = Template(body_template) if self.empty_body: del self.content_type del self.content_length def __str__(self): return self.detail or self.explanation def _make_body(self, environ, escape): args = { 'explanation': escape(self.explanation), 'detail': escape(self.detail or ''), 'comment': escape(self.comment or ''), } if self.comment: args['html_comment'] = '' % escape(self.comment) else: args['html_comment'] = '' if WSGIHTTPException.body_template_obj is not self.body_template_obj: # Custom template; add headers to args for k, v in environ.items(): args[k] = escape(v) for k, v in self.headers.items(): args[k.lower()] = escape(v) t_obj = self.body_template_obj return t_obj.substitute(args) def plain_body(self, environ): body = self._make_body(environ, no_escape) body = strip_tags(body) return self.plain_template_obj.substitute(status=self.status, title=self.title, body=body) def html_body(self, environ): body = self._make_body(environ, html_escape) return self.html_template_obj.substitute(status=self.status, body=body) def generate_response(self, environ, start_response): if self.content_length is not None: del self.content_length headerlist = list(self.headerlist) accept = environ.get('HTTP_ACCEPT', '') if accept and 'html' in accept or '*/*' in accept: content_type = 'text/html' body = self.html_body(environ) else: content_type = 'text/plain' body = self.plain_body(environ) extra_kw = {} if isinstance(body, text_type): extra_kw.update(charset='utf-8') resp = Response(body, status=self.status, headerlist=headerlist, content_type=content_type, **extra_kw ) resp.content_type = content_type return resp(environ, start_response) def __call__(self, environ, start_response): is_head = environ['REQUEST_METHOD'] == 'HEAD' if self.body or self.empty_body or is_head: app_iter = Response.__call__(self, environ, start_response) else: app_iter = self.generate_response(environ, start_response) if is_head: app_iter = [] return app_iter @property def wsgi_response(self): return self class HTTPError(WSGIHTTPException): """ base class for status codes in the 400's and 500's This is an exception which indicates that an error has occurred, and that any work in progress should not be committed. These are typically results in the 400's and 500's. """ class HTTPRedirection(WSGIHTTPException): """ base class for 300's status code (redirections) This is an abstract base class for 3xx redirection. It indicates that further action needs to be taken by the user agent in order to fulfill the request. It does not necessarly signal an error condition. """ class HTTPOk(WSGIHTTPException): """ Base class for the 200's status code (successful responses) code: 200, title: OK """ code = 200 title = 'OK' ############################################################ ## 2xx success ############################################################ class HTTPCreated(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that request has been fulfilled and resulted in a new resource being created. code: 201, title: Created """ code = 201 title = 'Created' class HTTPAccepted(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that the request has been accepted for processing, but the processing has not been completed. code: 202, title: Accepted """ code = 202 title = 'Accepted' explanation = 'The request is accepted for processing.' class HTTPNonAuthoritativeInformation(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that the returned metainformation in the entity-header is not the definitive set as available from the origin server, but is gathered from a local or a third-party copy. code: 203, title: Non-Authoritative Information """ code = 203 title = 'Non-Authoritative Information' class HTTPNoContent(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that the server has fulfilled the request but does not need to return an entity-body, and might want to return updated metainformation. code: 204, title: No Content """ code = 204 title = 'No Content' empty_body = True class HTTPResetContent(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that the the server has fulfilled the request and the user agent SHOULD reset the document view which caused the request to be sent. code: 205, title: Reset Content """ code = 205 title = 'Reset Content' empty_body = True class HTTPPartialContent(HTTPOk): """ subclass of :class:`~HTTPOk` This indicates that the server has fulfilled the partial GET request for the resource. code: 206, title: Partial Content """ code = 206 title = 'Partial Content' ############################################################ ## 3xx redirection ############################################################ class _HTTPMove(HTTPRedirection): """ redirections which require a Location field Since a 'Location' header is a required attribute of 301, 302, 303, 305 and 307 (but not 304), this base class provides the mechanics to make this easy. You can provide a location keyword argument to set the location immediately. You may also give ``add_slash=True`` if you want to redirect to the same URL as the request, except with a ``/`` added to the end. Relative URLs in the location will be resolved to absolute. """ explanation = 'The resource has been moved to' body_template_obj = Template('''\ ${explanation} ${location}; you should be redirected automatically. ${detail} ${html_comment}''') def __init__(self, detail=None, headers=None, comment=None, body_template=None, location=None, add_slash=False): super(_HTTPMove, self).__init__( detail=detail, headers=headers, comment=comment, body_template=body_template) if location is not None: self.location = location if add_slash: raise TypeError( "You can only provide one of the arguments location " "and add_slash") self.add_slash = add_slash def __call__(self, environ, start_response): req = Request(environ) if self.add_slash: url = req.path_url url += '/' if req.environ.get('QUERY_STRING'): url += '?' + req.environ['QUERY_STRING'] self.location = url self.location = urlparse.urljoin(req.path_url, self.location) return super(_HTTPMove, self).__call__( environ, start_response) class HTTPMultipleChoices(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the requested resource corresponds to any one of a set of representations, each with its own specific location, and agent-driven negotiation information is being provided so that the user can select a preferred representation and redirect its request to that location. code: 300, title: Multiple Choices """ code = 300 title = 'Multiple Choices' class HTTPMovedPermanently(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the requested resource has been assigned a new permanent URI and any future references to this resource SHOULD use one of the returned URIs. code: 301, title: Moved Permanently """ code = 301 title = 'Moved Permanently' class HTTPFound(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the requested resource resides temporarily under a different URI. code: 302, title: Found """ code = 302 title = 'Found' explanation = 'The resource was found at' # This one is safe after a POST (the redirected location will be # retrieved with GET): class HTTPSeeOther(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the response to the request can be found under a different URI and SHOULD be retrieved using a GET method on that resource. code: 303, title: See Other """ code = 303 title = 'See Other' class HTTPNotModified(HTTPRedirection): """ subclass of :class:`~HTTPRedirection` This indicates that if the client has performed a conditional GET request and access is allowed, but the document has not been modified, the server SHOULD respond with this status code. code: 304, title: Not Modified """ # TODO: this should include a date or etag header code = 304 title = 'Not Modified' empty_body = True class HTTPUseProxy(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the requested resource MUST be accessed through the proxy given by the Location field. code: 305, title: Use Proxy """ # Not a move, but looks a little like one code = 305 title = 'Use Proxy' explanation = ( 'The resource must be accessed through a proxy located at') class HTTPTemporaryRedirect(_HTTPMove): """ subclass of :class:`~_HTTPMove` This indicates that the requested resource resides temporarily under a different URI. code: 307, title: Temporary Redirect """ code = 307 title = 'Temporary Redirect' ############################################################ ## 4xx client error ############################################################ class HTTPClientError(HTTPError): """ base class for the 400's, where the client is in error This is an error condition in which the client is presumed to be in-error. This is an expected problem, and thus is not considered a bug. A server-side traceback is not warranted. Unless specialized, this is a '400 Bad Request' """ code = 400 title = 'Bad Request' explanation = ('The server could not comply with the request since\r\n' 'it is either malformed or otherwise incorrect.\r\n') class HTTPBadRequest(HTTPClientError): pass class HTTPUnauthorized(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the request requires user authentication. code: 401, title: Unauthorized """ code = 401 title = 'Unauthorized' explanation = ( 'This server could not verify that you are authorized to\r\n' 'access the document you requested. Either you supplied the\r\n' 'wrong credentials (e.g., bad password), or your browser\r\n' 'does not understand how to supply the credentials required.\r\n') class HTTPPaymentRequired(HTTPClientError): """ subclass of :class:`~HTTPClientError` code: 402, title: Payment Required """ code = 402 title = 'Payment Required' explanation = ('Access was denied for financial reasons.') class HTTPForbidden(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server understood the request, but is refusing to fulfill it. code: 403, title: Forbidden """ code = 403 title = 'Forbidden' explanation = ('Access was denied to this resource.') class HTTPNotFound(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server did not find anything matching the Request-URI. code: 404, title: Not Found """ code = 404 title = 'Not Found' explanation = ('The resource could not be found.') class HTTPMethodNotAllowed(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the method specified in the Request-Line is not allowed for the resource identified by the Request-URI. code: 405, title: Method Not Allowed """ code = 405 title = 'Method Not Allowed' # override template since we need an environment variable body_template_obj = Template('''\ The method ${REQUEST_METHOD} is not allowed for this resource.

${detail}''') class HTTPNotAcceptable(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates the resource identified by the request is only capable of generating response entities which have content characteristics not acceptable according to the accept headers sent in the request. code: 406, title: Not Acceptable """ code = 406 title = 'Not Acceptable' # override template since we need an environment variable template = Template('''\ The resource could not be generated that was acceptable to your browser (content of type ${HTTP_ACCEPT}.

${detail}''') class HTTPProxyAuthenticationRequired(HTTPClientError): """ subclass of :class:`~HTTPClientError` This is similar to 401, but indicates that the client must first authenticate itself with the proxy. code: 407, title: Proxy Authentication Required """ code = 407 title = 'Proxy Authentication Required' explanation = ('Authentication with a local proxy is needed.') class HTTPRequestTimeout(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the client did not produce a request within the time that the server was prepared to wait. code: 408, title: Request Timeout """ code = 408 title = 'Request Timeout' explanation = ('The server has waited too long for the request to ' 'be sent by the client.') class HTTPConflict(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the request could not be completed due to a conflict with the current state of the resource. code: 409, title: Conflict """ code = 409 title = 'Conflict' explanation = ('There was a conflict when trying to complete ' 'your request.') class HTTPGone(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the requested resource is no longer available at the server and no forwarding address is known. code: 410, title: Gone """ code = 410 title = 'Gone' explanation = ('This resource is no longer available. No forwarding ' 'address is given.') class HTTPLengthRequired(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the the server refuses to accept the request without a defined Content-Length. code: 411, title: Length Required """ code = 411 title = 'Length Required' explanation = ('Content-Length header required.') class HTTPPreconditionFailed(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the precondition given in one or more of the request-header fields evaluated to false when it was tested on the server. code: 412, title: Precondition Failed """ code = 412 title = 'Precondition Failed' explanation = ('Request precondition failed.') class HTTPRequestEntityTooLarge(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is refusing to process a request because the request entity is larger than the server is willing or able to process. code: 413, title: Request Entity Too Large """ code = 413 title = 'Request Entity Too Large' explanation = ('The body of your request was too large for this server.') class HTTPRequestURITooLong(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is refusing to service the request because the Request-URI is longer than the server is willing to interpret. code: 414, title: Request-URI Too Long """ code = 414 title = 'Request-URI Too Long' explanation = ('The request URI was too long for this server.') class HTTPUnsupportedMediaType(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is refusing to service the request because the entity of the request is in a format not supported by the requested resource for the requested method. code: 415, title: Unsupported Media Type """ code = 415 title = 'Unsupported Media Type' # override template since we need an environment variable template_obj = Template('''\ The request media type ${CONTENT_TYPE} is not supported by this server.

${detail}''') class HTTPRequestRangeNotSatisfiable(HTTPClientError): """ subclass of :class:`~HTTPClientError` The server SHOULD return a response with this status code if a request included a Range request-header field, and none of the range-specifier values in this field overlap the current extent of the selected resource, and the request did not include an If-Range request-header field. code: 416, title: Request Range Not Satisfiable """ code = 416 title = 'Request Range Not Satisfiable' explanation = ('The Range requested is not available.') class HTTPExpectationFailed(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indidcates that the expectation given in an Expect request-header field could not be met by this server. code: 417, title: Expectation Failed """ code = 417 title = 'Expectation Failed' explanation = ('Expectation failed.') class HTTPUnprocessableEntity(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is unable to process the contained instructions. Only for WebDAV. code: 422, title: Unprocessable Entity """ ## Note: from WebDAV code = 422 title = 'Unprocessable Entity' explanation = 'Unable to process the contained instructions' class HTTPLocked(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the resource is locked. Only for WebDAV code: 423, title: Locked """ ## Note: from WebDAV code = 423 title = 'Locked' explanation = ('The resource is locked') class HTTPFailedDependency(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the method could not be performed because the requested action depended on another action and that action failed. Only for WebDAV. code: 424, title: Failed Dependency """ ## Note: from WebDAV code = 424 title = 'Failed Dependency' explanation = ( 'The method could not be performed because the requested ' 'action dependended on another action and that action failed') class HTTPPreconditionRequired(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the origin server requires the request to be conditional. From RFC 6585, "Additional HTTP Status Codes". code: 428, title: Precondition Required """ code = 428 title = 'Precondition Required' explanation = ('This request is required to be conditional') class HTTPTooManyRequests(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the client has sent too many requests in a given amount of time. Useful for rate limiting. From RFC 6585, "Additional HTTP Status Codes". code: 429, title: Too Many Requests """ code = 429 title = 'Too Many Requests' explanation = ( 'The client has sent too many requests in a given amount of time') class HTTPRequestHeaderFieldsTooLarge(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is unwilling to process the request because its header fields are too large. The request may be resubmitted after reducing the size of the request header fields. From RFC 6585, "Additional HTTP Status Codes". code: 431, title: Request Header Fields Too Large """ code = 431 title = 'Request Header Fields Too Large' explanation = ( 'The request header fields were too large') class HTTPUnavailableForLegalReasons(HTTPClientError): """ subclass of :class:`~HTTPClientError` This indicates that the server is unable to process the request because of legal reasons, e.g. censorship or government-mandated blocked access. From the draft "A New HTTP Status Code for Legally-restricted Resources" by Tim Bray: http://tools.ietf.org/html/draft-tbray-http-legally-restricted-status-00 code: 451, title: Unavailable For Legal Reasons """ code = 451 title = 'Unavailable For Legal Reasons' explanation = ('The resource is not available due to legal reasons.') ############################################################ ## 5xx Server Error ############################################################ # Response status codes beginning with the digit "5" indicate cases in # which the server is aware that it has erred or is incapable of # performing the request. Except when responding to a HEAD request, the # server SHOULD include an entity containing an explanation of the error # situation, and whether it is a temporary or permanent condition. User # agents SHOULD display any included entity to the user. These response # codes are applicable to any request method. class HTTPServerError(HTTPError): """ base class for the 500's, where the server is in-error This is an error condition in which the server is presumed to be in-error. This is usually unexpected, and thus requires a traceback; ideally, opening a support ticket for the customer. Unless specialized, this is a '500 Internal Server Error' """ code = 500 title = 'Internal Server Error' explanation = ( 'The server has either erred or is incapable of performing\r\n' 'the requested operation.\r\n') class HTTPInternalServerError(HTTPServerError): pass class HTTPNotImplemented(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server does not support the functionality required to fulfill the request. code: 501, title: Not Implemented """ code = 501 title = 'Not Implemented' template = Template(''' The request method ${REQUEST_METHOD} is not implemented for this server.

${detail}''') class HTTPBadGateway(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server, while acting as a gateway or proxy, received an invalid response from the upstream server it accessed in attempting to fulfill the request. code: 502, title: Bad Gateway """ code = 502 title = 'Bad Gateway' explanation = ('Bad gateway.') class HTTPServiceUnavailable(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server is currently unable to handle the request due to a temporary overloading or maintenance of the server. code: 503, title: Service Unavailable """ code = 503 title = 'Service Unavailable' explanation = ('The server is currently unavailable. ' 'Please try again at a later time.') class HTTPGatewayTimeout(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server, while acting as a gateway or proxy, did not receive a timely response from the upstream server specified by the URI (e.g. HTTP, FTP, LDAP) or some other auxiliary server (e.g. DNS) it needed to access in attempting to complete the request. code: 504, title: Gateway Timeout """ code = 504 title = 'Gateway Timeout' explanation = ('The gateway has timed out.') class HTTPVersionNotSupported(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server does not support, or refuses to support, the HTTP protocol version that was used in the request message. code: 505, title: HTTP Version Not Supported """ code = 505 title = 'HTTP Version Not Supported' explanation = ('The HTTP version is not supported.') class HTTPInsufficientStorage(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the server does not have enough space to save the resource. code: 507, title: Insufficient Storage """ code = 507 title = 'Insufficient Storage' explanation = ('There was not enough space to save the resource') class HTTPNetworkAuthenticationRequired(HTTPServerError): """ subclass of :class:`~HTTPServerError` This indicates that the client needs to authenticate to gain network access. From RFC 6585, "Additional HTTP Status Codes". code: 511, title: Network Authentication Required """ code = 511 title = 'Network Authentication Required' explanation = ('Network authentication is required') class HTTPExceptionMiddleware(object): """ Middleware that catches exceptions in the sub-application. This does not catch exceptions in the app_iter; only during the initial calling of the application. This should be put *very close* to applications that might raise these exceptions. This should not be applied globally; letting *expected* exceptions raise through the WSGI stack is dangerous. """ def __init__(self, application): self.application = application def __call__(self, environ, start_response): try: return self.application(environ, start_response) except HTTPException: parent_exc_info = sys.exc_info() def repl_start_response(status, headers, exc_info=None): if exc_info is None: exc_info = parent_exc_info return start_response(status, headers, exc_info) return parent_exc_info[1](environ, repl_start_response) try: from paste import httpexceptions except ImportError: # pragma: no cover # Without Paste we don't need to do this fixup pass else: # pragma: no cover for name in dir(httpexceptions): obj = globals().get(name) if (obj and isinstance(obj, type) and issubclass(obj, HTTPException) and obj is not HTTPException and obj is not WSGIHTTPException): obj.__bases__ = obj.__bases__ + (getattr(httpexceptions, name),) del name, obj, httpexceptions __all__ = ['HTTPExceptionMiddleware', 'status_map'] status_map={} for name, value in list(globals().items()): if (isinstance(value, (type, class_types)) and issubclass(value, HTTPException) and not name.startswith('_')): __all__.append(name) if getattr(value, 'code', None): status_map[value.code]=value if hasattr(value, 'explanation'): value.explanation = ' '.join(value.explanation.strip().split()) del name, value WebOb-1.3.1/webob/multidict.py0000664000175000017500000003330412123261311016751 0ustar chrismchrism00000000000000# (c) 2005 Ian Bicking and contributors; written for Paste # (http://pythonpaste.org) Licensed under the MIT license: # http://www.opensource.org/licenses/mit-license.php """ Gives a multi-value dictionary object (MultiDict) plus several wrappers """ from collections import MutableMapping import binascii import warnings from webob.compat import ( PY3, iteritems_, itervalues_, url_encode, ) __all__ = ['MultiDict', 'NestedMultiDict', 'NoVars', 'GetDict'] class MultiDict(MutableMapping): """ An ordered dictionary that can have multiple values for each key. Adds the methods getall, getone, mixed and extend and add to the normal dictionary interface. """ def __init__(self, *args, **kw): if len(args) > 1: raise TypeError("MultiDict can only be called with one positional " "argument") if args: if hasattr(args[0], 'iteritems'): items = list(args[0].iteritems()) elif hasattr(args[0], 'items'): items = list(args[0].items()) else: items = list(args[0]) self._items = items else: self._items = [] if kw: self._items.extend(kw.items()) @classmethod def view_list(cls, lst): """ Create a dict that is a view on the given list """ if not isinstance(lst, list): raise TypeError( "%s.view_list(obj) takes only actual list objects, not %r" % (cls.__name__, lst)) obj = cls() obj._items = lst return obj @classmethod def from_fieldstorage(cls, fs): """ Create a dict from a cgi.FieldStorage instance """ obj = cls() # fs.list can be None when there's nothing to parse for field in fs.list or (): charset = field.type_options.get('charset', 'utf8') transfer_encoding = field.headers.get('Content-Transfer-Encoding', None) supported_tranfer_encoding = { 'base64' : binascii.a2b_base64, 'quoted-printable' : binascii.a2b_qp } if PY3: # pragma: no cover if charset == 'utf8': decode = lambda b: b else: decode = lambda b: b.encode('utf8').decode(charset) else: decode = lambda b: b.decode(charset) if field.filename: field.filename = decode(field.filename) obj.add(field.name, field) else: value = field.value if transfer_encoding in supported_tranfer_encoding: if PY3: # pragma: no cover # binascii accepts bytes value = value.encode('utf8') value = supported_tranfer_encoding[transfer_encoding](value) if PY3: # pragma: no cover # binascii returns bytes value = value.decode('utf8') obj.add(field.name, decode(value)) return obj def __getitem__(self, key): for k, v in reversed(self._items): if k == key: return v raise KeyError(key) def __setitem__(self, key, value): try: del self[key] except KeyError: pass self._items.append((key, value)) def add(self, key, value): """ Add the key and value, not overwriting any previous value. """ self._items.append((key, value)) def getall(self, key): """ Return a list of all values matching the key (may be an empty list) """ return [v for k, v in self._items if k == key] def getone(self, key): """ Get one value matching the key, raising a KeyError if multiple values were found. """ v = self.getall(key) if not v: raise KeyError('Key not found: %r' % key) if len(v) > 1: raise KeyError('Multiple values match %r: %r' % (key, v)) return v[0] def mixed(self): """ Returns a dictionary where the values are either single values, or a list of values when a key/value appears more than once in this dictionary. This is similar to the kind of dictionary often used to represent the variables in a web request. """ result = {} multi = {} for key, value in self.items(): if key in result: # We do this to not clobber any lists that are # *actual* values in this dictionary: if key in multi: result[key].append(value) else: result[key] = [result[key], value] multi[key] = None else: result[key] = value return result def dict_of_lists(self): """ Returns a dictionary where each key is associated with a list of values. """ r = {} for key, val in self.items(): r.setdefault(key, []).append(val) return r def __delitem__(self, key): items = self._items found = False for i in range(len(items)-1, -1, -1): if items[i][0] == key: del items[i] found = True if not found: raise KeyError(key) def __contains__(self, key): for k, v in self._items: if k == key: return True return False has_key = __contains__ def clear(self): del self._items[:] def copy(self): return self.__class__(self) def setdefault(self, key, default=None): for k, v in self._items: if key == k: return v self._items.append((key, default)) return default def pop(self, key, *args): if len(args) > 1: raise TypeError("pop expected at most 2 arguments, got %s" % repr(1 + len(args))) for i in range(len(self._items)): if self._items[i][0] == key: v = self._items[i][1] del self._items[i] return v if args: return args[0] else: raise KeyError(key) def popitem(self): return self._items.pop() def update(self, *args, **kw): if args: lst = args[0] if len(lst) != len(dict(lst)): # this does not catch the cases where we overwrite existing # keys, but those would produce too many warning msg = ("Behavior of MultiDict.update() has changed " "and overwrites duplicate keys. Consider using .extend()" ) warnings.warn(msg, UserWarning, stacklevel=2) MutableMapping.update(self, *args, **kw) def extend(self, other=None, **kwargs): if other is None: pass elif hasattr(other, 'items'): self._items.extend(other.items()) elif hasattr(other, 'keys'): for k in other.keys(): self._items.append((k, other[k])) else: for k, v in other: self._items.append((k, v)) if kwargs: self.update(kwargs) def __repr__(self): items = map('(%r, %r)'.__mod__, _hide_passwd(self.items())) return '%s([%s])' % (self.__class__.__name__, ', '.join(items)) def __len__(self): return len(self._items) ## ## All the iteration: ## def iterkeys(self): for k, v in self._items: yield k if PY3: # pragma: no cover keys = iterkeys else: def keys(self): return [k for k, v in self._items] __iter__ = iterkeys def iteritems(self): return iter(self._items) if PY3: # pragma: no cover items = iteritems else: def items(self): return self._items[:] def itervalues(self): for k, v in self._items: yield v if PY3: # pragma: no cover values = itervalues else: def values(self): return [v for k, v in self._items] _dummy = object() class GetDict(MultiDict): # def __init__(self, data, tracker, encoding, errors): # d = lambda b: b.decode(encoding, errors) # data = [(d(k), d(v)) for k,v in data] def __init__(self, data, env): self.env = env MultiDict.__init__(self, data) def on_change(self): e = lambda t: t.encode('utf8') data = [(e(k), e(v)) for k,v in self.items()] qs = url_encode(data) self.env['QUERY_STRING'] = qs self.env['webob._parsed_query_vars'] = (self, qs) def __setitem__(self, key, value): MultiDict.__setitem__(self, key, value) self.on_change() def add(self, key, value): MultiDict.add(self, key, value) self.on_change() def __delitem__(self, key): MultiDict.__delitem__(self, key) self.on_change() def clear(self): MultiDict.clear(self) self.on_change() def setdefault(self, key, default=None): result = MultiDict.setdefault(self, key, default) self.on_change() return result def pop(self, key, *args): result = MultiDict.pop(self, key, *args) self.on_change() return result def popitem(self): result = MultiDict.popitem(self) self.on_change() return result def update(self, *args, **kwargs): MultiDict.update(self, *args, **kwargs) self.on_change() def __repr__(self): items = map('(%r, %r)'.__mod__, _hide_passwd(self.items())) # TODO: GET -> GetDict return 'GET([%s])' % (', '.join(items)) def copy(self): # Copies shouldn't be tracked return MultiDict(self) class NestedMultiDict(MultiDict): """ Wraps several MultiDict objects, treating it as one large MultiDict """ def __init__(self, *dicts): self.dicts = dicts def __getitem__(self, key): for d in self.dicts: value = d.get(key, _dummy) if value is not _dummy: return value raise KeyError(key) def _readonly(self, *args, **kw): raise KeyError("NestedMultiDict objects are read-only") __setitem__ = _readonly add = _readonly __delitem__ = _readonly clear = _readonly setdefault = _readonly pop = _readonly popitem = _readonly update = _readonly def getall(self, key): result = [] for d in self.dicts: result.extend(d.getall(key)) return result # Inherited: # getone # mixed # dict_of_lists def copy(self): return MultiDict(self) def __contains__(self, key): for d in self.dicts: if key in d: return True return False has_key = __contains__ def __len__(self): v = 0 for d in self.dicts: v += len(d) return v def __nonzero__(self): for d in self.dicts: if d: return True return False def iteritems(self): for d in self.dicts: for item in iteritems_(d): yield item if PY3: # pragma: no cover items = iteritems else: def items(self): return list(self.iteritems()) def itervalues(self): for d in self.dicts: for value in itervalues_(d): yield value if PY3: # pragma: no cover values = itervalues else: def values(self): return list(self.itervalues()) def __iter__(self): for d in self.dicts: for key in d: yield key iterkeys = __iter__ if PY3: # pragma: no cover keys = iterkeys else: def keys(self): return list(self.iterkeys()) class NoVars(object): """ Represents no variables; used when no variables are applicable. This is read-only """ def __init__(self, reason=None): self.reason = reason or 'N/A' def __getitem__(self, key): raise KeyError("No key %r: %s" % (key, self.reason)) def __setitem__(self, *args, **kw): raise KeyError("Cannot add variables: %s" % self.reason) add = __setitem__ setdefault = __setitem__ update = __setitem__ def __delitem__(self, *args, **kw): raise KeyError("No keys to delete: %s" % self.reason) clear = __delitem__ pop = __delitem__ popitem = __delitem__ def get(self, key, default=None): return default def getall(self, key): return [] def getone(self, key): return self[key] def mixed(self): return {} dict_of_lists = mixed def __contains__(self, key): return False has_key = __contains__ def copy(self): return self def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.reason) def __len__(self): return 0 def __cmp__(self, other): return cmp({}, other) def iterkeys(self): return iter([]) if PY3: # pragma: no cover keys = iterkeys items = iterkeys values = iterkeys else: def keys(self): return [] items = keys values = keys itervalues = iterkeys iteritems = iterkeys __iter__ = iterkeys def _hide_passwd(items): for k, v in items: if ('password' in k or 'passwd' in k or 'pwd' in k ): yield k, '******' else: yield k, v WebOb-1.3.1/webob/request.py0000664000175000017500000016516212245462656016477 0ustar chrismchrism00000000000000import binascii import cgi import io import os import re import sys import tempfile import mimetypes try: import simplejson as json except ImportError: import json import warnings from webob.acceptparse import ( AcceptLanguage, AcceptCharset, MIMEAccept, MIMENilAccept, NoAccept, accept_property, ) from webob.cachecontrol import ( CacheControl, serialize_cache_control, ) from webob.compat import ( PY3, bytes_, integer_types, native_, parse_qsl_text, reraise, text_type, url_encode, url_quote, url_unquote, quote_plus, urlparse, ) from webob.cookies import RequestCookies from webob.descriptors import ( CHARSET_RE, SCHEME_RE, converter, converter_date, environ_getter, environ_decoder, parse_auth, parse_int, parse_int_safe, parse_range, serialize_auth, serialize_if_range, serialize_int, serialize_range, upath_property, deprecated_property, ) from webob.etag import ( IfRange, AnyETag, NoETag, etag_property, ) from webob.headers import EnvironHeaders from webob.multidict import ( NestedMultiDict, MultiDict, NoVars, GetDict, ) from webob.util import warn_deprecation __all__ = ['BaseRequest', 'Request', 'LegacyRequest'] class _NoDefault: def __repr__(self): return '(No Default)' NoDefault = _NoDefault() PATH_SAFE = '/:@&+$,' http_method_probably_has_body = dict.fromkeys( ('GET', 'HEAD', 'DELETE', 'TRACE'), False) http_method_probably_has_body.update( dict.fromkeys(('POST', 'PUT', 'PATCH'), True)) _LATIN_ENCODINGS = ( 'ascii', 'latin-1', 'latin', 'latin_1', 'l1', 'latin1', 'iso-8859-1', 'iso8859_1', 'iso_8859_1', 'iso8859', '8859', ) class BaseRequest(object): ## The limit after which request bodies should be stored on disk ## if they are read in (under this, and the request body is stored ## in memory): request_body_tempfile_limit = 10*1024 _charset = None def __init__(self, environ, charset=None, unicode_errors=None, decode_param_names=None, **kw): if type(environ) is not dict: raise TypeError( "WSGI environ must be a dict; you passed %r" % (environ,)) if unicode_errors is not None: warnings.warn( "You unicode_errors=%r to the Request constructor. Passing a " "``unicode_errors`` value to the Request is no longer " "supported in WebOb 1.2+. This value has been ignored " % ( unicode_errors,), DeprecationWarning ) if decode_param_names is not None: warnings.warn( "You passed decode_param_names=%r to the Request constructor. " "Passing a ``decode_param_names`` value to the Request " "is no longer supported in WebOb 1.2+. This value has " "been ignored " % (decode_param_names,), DeprecationWarning ) if not _is_utf8(charset): raise DeprecationWarning( "You passed charset=%r to the Request constructor. As of " "WebOb 1.2, if your application needs a non-UTF-8 request " "charset, please construct the request without a charset or " "with a charset of 'None', then use ``req = " "req.decode(charset)``" % charset ) d = self.__dict__ d['environ'] = environ if kw: cls = self.__class__ if 'method' in kw: # set method first, because .body setters # depend on it for checks self.method = kw.pop('method') for name, value in kw.items(): if not hasattr(cls, name): raise TypeError( "Unexpected keyword: %s=%r" % (name, value)) setattr(self, name, value) if PY3: # pragma: no cover def encget(self, key, default=NoDefault, encattr=None): val = self.environ.get(key, default) if val is NoDefault: raise KeyError(key) if val is default: return default if not encattr: return val encoding = getattr(self, encattr) if encoding in _LATIN_ENCODINGS: # shortcut return val return bytes_(val, 'latin-1').decode(encoding) else: def encget(self, key, default=NoDefault, encattr=None): val = self.environ.get(key, default) if val is NoDefault: raise KeyError(key) if val is default: return default if encattr is None: return val encoding = getattr(self, encattr) return val.decode(encoding) def encset(self, key, val, encattr=None): if encattr: encoding = getattr(self, encattr) else: encoding = 'ascii' if PY3: # pragma: no cover self.environ[key] = bytes_(val, encoding).decode('latin-1') else: self.environ[key] = bytes_(val, encoding) @property def charset(self): if self._charset is None: charset = detect_charset(self._content_type_raw) if _is_utf8(charset): charset = 'UTF-8' self._charset = charset return self._charset @charset.setter def charset(self, charset): if _is_utf8(charset): charset = 'UTF-8' if charset != self.charset: raise DeprecationWarning("Use req = req.decode(%r)" % charset) def decode(self, charset=None, errors='strict'): charset = charset or self.charset if charset == 'UTF-8': return self # cookies and path are always utf-8 t = Transcoder(charset, errors) new_content_type = CHARSET_RE.sub('; charset="UTF-8"', self._content_type_raw) content_type = self.content_type r = self.__class__( self.environ.copy(), query_string=t.transcode_query(self.query_string), content_type=new_content_type, ) if content_type == 'application/x-www-form-urlencoded': r.body = bytes_(t.transcode_query(native_(r.body))) return r elif content_type != 'multipart/form-data': return r fs_environ = self.environ.copy() fs_environ.setdefault('CONTENT_LENGTH', '0') fs_environ['QUERY_STRING'] = '' if PY3: # pragma: no cover fs = cgi.FieldStorage(fp=self.body_file, environ=fs_environ, keep_blank_values=True, encoding=charset, errors=errors) else: fs = cgi.FieldStorage(fp=self.body_file, environ=fs_environ, keep_blank_values=True) fout = t.transcode_fs(fs, r._content_type_raw) # this order is important, because setting body_file # resets content_length r.body_file = fout r.content_length = fout.tell() fout.seek(0) return r # this is necessary for correct warnings depth for both # BaseRequest and Request (due to AdhocAttrMixin.__setattr__) _setattr_stacklevel = 2 def _body_file__get(self): """ Input stream of the request (wsgi.input). Setting this property resets the content_length and seekable flag (unlike setting req.body_file_raw). """ if not self.is_body_readable: return io.BytesIO() r = self.body_file_raw clen = self.content_length if not self.is_body_seekable and clen is not None: # we need to wrap input in LimitedLengthFile # but we have to cache the instance as well # otherwise this would stop working # (.remaining counter would reset between calls): # req.body_file.read(100) # req.body_file.read(100) env = self.environ wrapped, raw = env.get('webob._body_file', (0,0)) if raw is not r: wrapped = LimitedLengthFile(r, clen) wrapped = io.BufferedReader(wrapped) env['webob._body_file'] = wrapped, r r = wrapped return r def _body_file__set(self, value): if isinstance(value, bytes): warn_deprecation( "Please use req.body = b'bytes' or req.body_file = fileobj", '1.2', self._setattr_stacklevel ) self.content_length = None self.body_file_raw = value self.is_body_seekable = False self.is_body_readable = True def _body_file__del(self): self.body = b'' body_file = property(_body_file__get, _body_file__set, _body_file__del, doc=_body_file__get.__doc__) body_file_raw = environ_getter('wsgi.input') @property def body_file_seekable(self): """ Get the body of the request (wsgi.input) as a seekable file-like object. Middleware and routing applications should use this attribute over .body_file. If you access this value, CONTENT_LENGTH will also be updated. """ if not self.is_body_seekable: self.make_body_seekable() return self.body_file_raw url_encoding = environ_getter('webob.url_encoding', 'UTF-8') scheme = environ_getter('wsgi.url_scheme') method = environ_getter('REQUEST_METHOD', 'GET') http_version = environ_getter('SERVER_PROTOCOL') content_length = converter( environ_getter('CONTENT_LENGTH', None, '14.13'), parse_int_safe, serialize_int, 'int') remote_user = environ_getter('REMOTE_USER', None) remote_addr = environ_getter('REMOTE_ADDR', None) query_string = environ_getter('QUERY_STRING', '') server_name = environ_getter('SERVER_NAME') server_port = converter( environ_getter('SERVER_PORT'), parse_int, serialize_int, 'int') script_name = environ_decoder('SCRIPT_NAME', '', encattr='url_encoding') path_info = environ_decoder('PATH_INFO', encattr='url_encoding') # bw compat uscript_name = script_name upath_info = path_info _content_type_raw = environ_getter('CONTENT_TYPE', '') def _content_type__get(self): """Return the content type, but leaving off any parameters (like charset, but also things like the type in ``application/atom+xml; type=entry``) If you set this property, you can include parameters, or if you don't include any parameters in the value then existing parameters will be preserved. """ return self._content_type_raw.split(';', 1)[0] def _content_type__set(self, value=None): if value is not None: value = str(value) if ';' not in value: content_type = self._content_type_raw if ';' in content_type: value += ';' + content_type.split(';', 1)[1] self._content_type_raw = value content_type = property(_content_type__get, _content_type__set, _content_type__set, _content_type__get.__doc__) _headers = None def _headers__get(self): """ All the request headers as a case-insensitive dictionary-like object. """ if self._headers is None: self._headers = EnvironHeaders(self.environ) return self._headers def _headers__set(self, value): self.headers.clear() self.headers.update(value) headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__) @property def client_addr(self): """ The effective client IP address as a string. If the ``HTTP_X_FORWARDED_FOR`` header exists in the WSGI environ, this attribute returns the client IP address present in that header (e.g. if the header value is ``192.168.1.1, 192.168.1.2``, the value will be ``192.168.1.1``). If no ``HTTP_X_FORWARDED_FOR`` header is present in the environ at all, this attribute will return the value of the ``REMOTE_ADDR`` header. If the ``REMOTE_ADDR`` header is unset, this attribute will return the value ``None``. .. warning:: It is possible for user agents to put someone else's IP or just any string in ``HTTP_X_FORWARDED_FOR`` as it is a normal HTTP header. Forward proxies can also provide incorrect values (private IP addresses etc). You cannot "blindly" trust the result of this method to provide you with valid data unless you're certain that ``HTTP_X_FORWARDED_FOR`` has the correct values. The WSGI server must be behind a trusted proxy for this to be true. """ e = self.environ xff = e.get('HTTP_X_FORWARDED_FOR') if xff is not None: addr = xff.split(',')[0].strip() else: addr = e.get('REMOTE_ADDR') return addr @property def host_port(self): """ The effective server port number as a string. If the ``HTTP_HOST`` header exists in the WSGI environ, this attribute returns the port number present in that header. If the ``HTTP_HOST`` header exists but contains no explicit port number: if the WSGI url scheme is "https" , this attribute returns "443", if the WSGI url scheme is "http", this attribute returns "80" . If no ``HTTP_HOST`` header is present in the environ at all, this attribute will return the value of the ``SERVER_PORT`` header (which is guaranteed to be present). """ e = self.environ host = e.get('HTTP_HOST') if host is not None: if ':' in host: host, port = host.split(':', 1) else: url_scheme = e['wsgi.url_scheme'] if url_scheme == 'https': port = '443' else: port = '80' else: port = e['SERVER_PORT'] return port @property def host_url(self): """ The URL through the host (no path) """ e = self.environ scheme = e.get('wsgi.url_scheme') url = scheme + '://' host = e.get('HTTP_HOST') if host is not None: if ':' in host: host, port = host.split(':', 1) else: port = None else: host = e.get('SERVER_NAME') port = e.get('SERVER_PORT') if scheme == 'https': if port == '443': port = None elif scheme == 'http': if port == '80': port = None url += host if port: url += ':%s' % port return url @property def application_url(self): """ The URL including SCRIPT_NAME (no PATH_INFO or query string) """ bscript_name = bytes_(self.script_name, self.url_encoding) return self.host_url + url_quote(bscript_name, PATH_SAFE) @property def path_url(self): """ The URL including SCRIPT_NAME and PATH_INFO, but not QUERY_STRING """ bpath_info = bytes_(self.path_info, self.url_encoding) return self.application_url + url_quote(bpath_info, PATH_SAFE) @property def path(self): """ The path of the request, without host or query string """ bscript = bytes_(self.script_name, self.url_encoding) bpath = bytes_(self.path_info, self.url_encoding) return url_quote(bscript, PATH_SAFE) + url_quote(bpath, PATH_SAFE) @property def path_qs(self): """ The path of the request, without host but with query string """ path = self.path qs = self.environ.get('QUERY_STRING') if qs: path += '?' + qs return path @property def url(self): """ The full request URL, including QUERY_STRING """ url = self.path_url qs = self.environ.get('QUERY_STRING') if qs: url += '?' + qs return url def relative_url(self, other_url, to_application=False): """ Resolve other_url relative to the request URL. If ``to_application`` is True, then resolve it relative to the URL with only SCRIPT_NAME """ if to_application: url = self.application_url if not url.endswith('/'): url += '/' else: url = self.path_url return urlparse.urljoin(url, other_url) def path_info_pop(self, pattern=None): """ 'Pops' off the next segment of PATH_INFO, pushing it onto SCRIPT_NAME, and returning the popped segment. Returns None if there is nothing left on PATH_INFO. Does not return ``''`` when there's an empty segment (like ``/path//path``); these segments are just ignored. Optional ``pattern`` argument is a regexp to match the return value before returning. If there is no match, no changes are made to the request and None is returned. """ path = self.path_info if not path: return None slashes = '' while path.startswith('/'): slashes += '/' path = path[1:] idx = path.find('/') if idx == -1: idx = len(path) r = path[:idx] if pattern is None or re.match(pattern, r): self.script_name += slashes + r self.path_info = path[idx:] return r def path_info_peek(self): """ Returns the next segment on PATH_INFO, or None if there is no next segment. Doesn't modify the environment. """ path = self.path_info if not path: return None path = path.lstrip('/') return path.split('/', 1)[0] def _urlvars__get(self): """ Return any *named* variables matched in the URL. Takes values from ``environ['wsgiorg.routing_args']``. Systems like ``routes`` set this value. """ if 'paste.urlvars' in self.environ: return self.environ['paste.urlvars'] elif 'wsgiorg.routing_args' in self.environ: return self.environ['wsgiorg.routing_args'][1] else: result = {} self.environ['wsgiorg.routing_args'] = ((), result) return result def _urlvars__set(self, value): environ = self.environ if 'wsgiorg.routing_args' in environ: environ['wsgiorg.routing_args'] = ( environ['wsgiorg.routing_args'][0], value) if 'paste.urlvars' in environ: del environ['paste.urlvars'] elif 'paste.urlvars' in environ: environ['paste.urlvars'] = value else: environ['wsgiorg.routing_args'] = ((), value) def _urlvars__del(self): if 'paste.urlvars' in self.environ: del self.environ['paste.urlvars'] if 'wsgiorg.routing_args' in self.environ: if not self.environ['wsgiorg.routing_args'][0]: del self.environ['wsgiorg.routing_args'] else: self.environ['wsgiorg.routing_args'] = ( self.environ['wsgiorg.routing_args'][0], {}) urlvars = property(_urlvars__get, _urlvars__set, _urlvars__del, doc=_urlvars__get.__doc__) def _urlargs__get(self): """ Return any *positional* variables matched in the URL. Takes values from ``environ['wsgiorg.routing_args']``. Systems like ``routes`` set this value. """ if 'wsgiorg.routing_args' in self.environ: return self.environ['wsgiorg.routing_args'][0] else: # Since you can't update this value in-place, we don't need # to set the key in the environment return () def _urlargs__set(self, value): environ = self.environ if 'paste.urlvars' in environ: # Some overlap between this and wsgiorg.routing_args; we need # wsgiorg.routing_args to make this work routing_args = (value, environ.pop('paste.urlvars')) elif 'wsgiorg.routing_args' in environ: routing_args = (value, environ['wsgiorg.routing_args'][1]) else: routing_args = (value, {}) environ['wsgiorg.routing_args'] = routing_args def _urlargs__del(self): if 'wsgiorg.routing_args' in self.environ: if not self.environ['wsgiorg.routing_args'][1]: del self.environ['wsgiorg.routing_args'] else: self.environ['wsgiorg.routing_args'] = ( (), self.environ['wsgiorg.routing_args'][1]) urlargs = property(_urlargs__get, _urlargs__set, _urlargs__del, _urlargs__get.__doc__) @property def is_xhr(self): """Is X-Requested-With header present and equal to ``XMLHttpRequest``? Note: this isn't set by every XMLHttpRequest request, it is only set if you are using a Javascript library that sets it (or you set the header yourself manually). Currently Prototype and jQuery are known to set this header.""" return self.environ.get('HTTP_X_REQUESTED_WITH', '') == 'XMLHttpRequest' def _host__get(self): """Host name provided in HTTP_HOST, with fall-back to SERVER_NAME""" if 'HTTP_HOST' in self.environ: return self.environ['HTTP_HOST'] else: return '%(SERVER_NAME)s:%(SERVER_PORT)s' % self.environ def _host__set(self, value): self.environ['HTTP_HOST'] = value def _host__del(self): if 'HTTP_HOST' in self.environ: del self.environ['HTTP_HOST'] host = property(_host__get, _host__set, _host__del, doc=_host__get.__doc__) @property def domain(self): """ Returns the domain portion of the host value. Equivalent to: .. code-block:: python domain = request.host if ':' in domain: domain = domain.split(':', 1)[0] This will be equivalent to the domain portion of the ``HTTP_HOST`` value in the environment if it exists, or the ``SERVER_NAME`` value in the environment if it doesn't. For example, if the environment contains an ``HTTP_HOST`` value of ``foo.example.com:8000``, ``request.domain`` will return ``foo.example.com``. Note that this value cannot be *set* on the request. To set the host value use :meth:`webob.request.Request.host` instead. """ domain = self.host if ':' in domain: domain = domain.split(':', 1)[0] return domain def _body__get(self): """ Return the content of the request body. """ if not self.is_body_readable: return b'' self.make_body_seekable() # we need this to have content_length r = self.body_file.read(self.content_length) self.body_file_raw.seek(0) return r def _body__set(self, value): if value is None: value = b'' if not isinstance(value, bytes): raise TypeError("You can only set Request.body to bytes (not %r)" % type(value)) if not http_method_probably_has_body.get(self.method, True): if not value: self.content_length = None self.body_file_raw = io.BytesIO() return self.content_length = len(value) self.body_file_raw = io.BytesIO(value) self.is_body_seekable = True def _body__del(self): self.body = b'' body = property(_body__get, _body__set, _body__del, doc=_body__get.__doc__) def _json_body__get(self): """Access the body of the request as JSON""" return json.loads(self.body.decode(self.charset)) def _json_body__set(self, value): self.body = json.dumps(value, separators=(',', ':')).encode(self.charset) def _json_body__del(self): del self.body json = json_body = property(_json_body__get, _json_body__set, _json_body__del) def _text__get(self): """ Get/set the text value of the body """ if not self.charset: raise AttributeError( "You cannot access Request.text unless charset is set") body = self.body return body.decode(self.charset) def _text__set(self, value): if not self.charset: raise AttributeError( "You cannot access Response.text unless charset is set") if not isinstance(value, text_type): raise TypeError( "You can only set Request.text to a unicode string " "(not %s)" % type(value)) self.body = value.encode(self.charset) def _text__del(self): del self.body text = property(_text__get, _text__set, _text__del, doc=_text__get.__doc__) @property def POST(self): """ Return a MultiDict containing all the variables from a form request. Returns an empty dict-like object for non-form requests. Form requests are typically POST requests, however PUT & PATCH requests with an appropriate Content-Type are also supported. """ env = self.environ if self.method not in ('POST', 'PUT', 'PATCH'): return NoVars('Not a form request') if 'webob._parsed_post_vars' in env: vars, body_file = env['webob._parsed_post_vars'] if body_file is self.body_file_raw: return vars content_type = self.content_type if ((self.method == 'PUT' and not content_type) or content_type not in ('', 'application/x-www-form-urlencoded', 'multipart/form-data') ): # Not an HTML form submission return NoVars('Not an HTML form submission (Content-Type: %s)' % content_type) self._check_charset() if self.is_body_seekable: self.body_file_raw.seek(0) fs_environ = env.copy() # FieldStorage assumes a missing CONTENT_LENGTH, but a # default of 0 is better: fs_environ.setdefault('CONTENT_LENGTH', '0') fs_environ['QUERY_STRING'] = '' if PY3: # pragma: no cover fs = cgi.FieldStorage( fp=self.body_file, environ=fs_environ, keep_blank_values=True, encoding='utf8') vars = MultiDict.from_fieldstorage(fs) else: fs = cgi.FieldStorage( fp=self.body_file, environ=fs_environ, keep_blank_values=True) vars = MultiDict.from_fieldstorage(fs) #ctype = self.content_type or 'application/x-www-form-urlencoded' ctype = self._content_type_raw or 'application/x-www-form-urlencoded' f = FakeCGIBody(vars, ctype) self.body_file = io.BufferedReader(f) env['webob._parsed_post_vars'] = (vars, self.body_file_raw) return vars @property def GET(self): """ Return a MultiDict containing all the variables from the QUERY_STRING. """ env = self.environ source = env.get('QUERY_STRING', '') if 'webob._parsed_query_vars' in env: vars, qs = env['webob._parsed_query_vars'] if qs == source: return vars data = [] if source: # this is disabled because we want to access req.GET # for text/plain; charset=ascii uploads for example #self._check_charset() data = parse_qsl_text(source) #d = lambda b: b.decode('utf8') #data = [(d(k), d(v)) for k,v in data] vars = GetDict(data, env) env['webob._parsed_query_vars'] = (vars, source) return vars def _check_charset(self): if self.charset != 'UTF-8': raise DeprecationWarning( "Requests are expected to be submitted in UTF-8, not %s. " "You can fix this by doing req = req.decode('%s')" % ( self.charset, self.charset) ) @property def params(self): """ A dictionary-like object containing both the parameters from the query string and request body. """ params = NestedMultiDict(self.GET, self.POST) return params @property def cookies(self): """ Return a dictionary of cookies as found in the request. """ return RequestCookies(self.environ) @cookies.setter def cookies(self, val): self.environ.pop('HTTP_COOKIE', None) r = RequestCookies(self.environ) r.update(val) def copy(self): """ Copy the request and environment object. This only does a shallow copy, except of wsgi.input """ self.make_body_seekable() env = self.environ.copy() new_req = self.__class__(env) new_req.copy_body() return new_req def copy_get(self): """ Copies the request and environment object, but turning this request into a GET along the way. If this was a POST request (or any other verb) then it becomes GET, and the request body is thrown away. """ env = self.environ.copy() return self.__class__(env, method='GET', content_type=None, body=b'') # webob.is_body_seekable marks input streams that are seekable # this way we can have seekable input without testing the .seek() method is_body_seekable = environ_getter('webob.is_body_seekable', False) #is_body_readable = environ_getter('webob.is_body_readable', False) def _is_body_readable__get(self): """ webob.is_body_readable is a flag that tells us that we can read the input stream even though CONTENT_LENGTH is missing. This allows FakeCGIBody to work and can be used by servers to support chunked encoding in requests. For background see https://bitbucket.org/ianb/webob/issue/6 """ if http_method_probably_has_body.get(self.method): # known HTTP method with body return True elif self.content_length is not None: # unknown HTTP method, but the Content-Length # header is present return True else: # last resort -- rely on the special flag return self.environ.get('webob.is_body_readable', False) def _is_body_readable__set(self, flag): self.environ['webob.is_body_readable'] = bool(flag) is_body_readable = property(_is_body_readable__get, _is_body_readable__set, doc=_is_body_readable__get.__doc__ ) def make_body_seekable(self): """ This forces ``environ['wsgi.input']`` to be seekable. That means that, the content is copied into a BytesIO or temporary file and flagged as seekable, so that it will not be unnecessarily copied again. After calling this method the .body_file is always seeked to the start of file and .content_length is not None. The choice to copy to BytesIO is made from ``self.request_body_tempfile_limit`` """ if self.is_body_seekable: self.body_file_raw.seek(0) else: self.copy_body() def copy_body(self): """ Copies the body, in cases where it might be shared with another request object and that is not desired. This copies the body in-place, either into a BytesIO object or a temporary file. """ if not self.is_body_readable: # there's no body to copy self.body = b'' elif self.content_length is None: # chunked body or FakeCGIBody self.body = self.body_file_raw.read() self._copy_body_tempfile() else: # try to read body into tempfile did_copy = self._copy_body_tempfile() if not did_copy: # it wasn't necessary, so just read it into memory self.body = self.body_file.read(self.content_length) def _copy_body_tempfile(self): """ Copy wsgi.input to tempfile if necessary. Returns True if it did. """ tempfile_limit = self.request_body_tempfile_limit todo = self.content_length assert isinstance(todo, integer_types), todo if not tempfile_limit or todo <= tempfile_limit: return False fileobj = self.make_tempfile() input = self.body_file while todo > 0: data = input.read(min(todo, 65536)) if not data: # Normally this should not happen, because LimitedLengthFile # should have raised an exception by now. # It can happen if the is_body_seekable flag is incorrect. raise DisconnectionError( "Client disconnected (%s more bytes were expected)" % todo ) fileobj.write(data) todo -= len(data) fileobj.seek(0) self.body_file_raw = fileobj self.is_body_seekable = True return True def make_tempfile(self): """ Create a tempfile to store big request body. This API is not stable yet. A 'size' argument might be added. """ return tempfile.TemporaryFile() def remove_conditional_headers(self, remove_encoding=True, remove_range=True, remove_match=True, remove_modified=True): """ Remove headers that make the request conditional. These headers can cause the response to be 304 Not Modified, which in some cases you may not want to be possible. This does not remove headers like If-Match, which are used for conflict detection. """ check_keys = [] if remove_range: check_keys += ['HTTP_IF_RANGE', 'HTTP_RANGE'] if remove_match: check_keys.append('HTTP_IF_NONE_MATCH') if remove_modified: check_keys.append('HTTP_IF_MODIFIED_SINCE') if remove_encoding: check_keys.append('HTTP_ACCEPT_ENCODING') for key in check_keys: if key in self.environ: del self.environ[key] accept = accept_property('Accept', '14.1', MIMEAccept, MIMENilAccept) accept_charset = accept_property('Accept-Charset', '14.2', AcceptCharset) accept_encoding = accept_property('Accept-Encoding', '14.3', NilClass=NoAccept) accept_language = accept_property('Accept-Language', '14.4', AcceptLanguage) authorization = converter( environ_getter('HTTP_AUTHORIZATION', None, '14.8'), parse_auth, serialize_auth, ) def _cache_control__get(self): """ Get/set/modify the Cache-Control header (`HTTP spec section 14.9 `_) """ env = self.environ value = env.get('HTTP_CACHE_CONTROL', '') cache_header, cache_obj = env.get('webob._cache_control', (None, None)) if cache_obj is not None and cache_header == value: return cache_obj cache_obj = CacheControl.parse(value, updates_to=self._update_cache_control, type='request') env['webob._cache_control'] = (value, cache_obj) return cache_obj def _cache_control__set(self, value): env = self.environ value = value or '' if isinstance(value, dict): value = CacheControl(value, type='request') if isinstance(value, CacheControl): str_value = str(value) env['HTTP_CACHE_CONTROL'] = str_value env['webob._cache_control'] = (str_value, value) else: env['HTTP_CACHE_CONTROL'] = str(value) env['webob._cache_control'] = (None, None) def _cache_control__del(self): env = self.environ if 'HTTP_CACHE_CONTROL' in env: del env['HTTP_CACHE_CONTROL'] if 'webob._cache_control' in env: del env['webob._cache_control'] def _update_cache_control(self, prop_dict): self.environ['HTTP_CACHE_CONTROL'] = serialize_cache_control(prop_dict) cache_control = property(_cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__) if_match = etag_property('HTTP_IF_MATCH', AnyETag, '14.24') if_none_match = etag_property('HTTP_IF_NONE_MATCH', NoETag, '14.26', strong=False) date = converter_date(environ_getter('HTTP_DATE', None, '14.8')) if_modified_since = converter_date( environ_getter('HTTP_IF_MODIFIED_SINCE', None, '14.25')) if_unmodified_since = converter_date( environ_getter('HTTP_IF_UNMODIFIED_SINCE', None, '14.28')) if_range = converter( environ_getter('HTTP_IF_RANGE', None, '14.27'), IfRange.parse, serialize_if_range, 'IfRange object') max_forwards = converter( environ_getter('HTTP_MAX_FORWARDS', None, '14.31'), parse_int, serialize_int, 'int') pragma = environ_getter('HTTP_PRAGMA', None, '14.32') range = converter( environ_getter('HTTP_RANGE', None, '14.35'), parse_range, serialize_range, 'Range object') referer = environ_getter('HTTP_REFERER', None, '14.36') referrer = referer user_agent = environ_getter('HTTP_USER_AGENT', None, '14.43') def __repr__(self): try: name = '%s %s' % (self.method, self.url) except KeyError: name = '(invalid WSGI environ)' msg = '<%s at 0x%x %s>' % ( self.__class__.__name__, abs(id(self)), name) return msg def as_bytes(self, skip_body=False): """ Return HTTP bytes representing this request. If skip_body is True, exclude the body. If skip_body is an integer larger than one, skip body only if its length is bigger than that number. """ url = self.url host = self.host_url assert url.startswith(host) url = url[len(host):] parts = [bytes_('%s %s %s' % (self.method, url, self.http_version))] #self.headers.setdefault('Host', self.host) # acquire body before we handle headers so that # content-length will be set body = None if self.method in ('PUT', 'POST'): if skip_body > 1: if len(self.body) > skip_body: body = bytes_('' % len(self.body)) else: skip_body = False if not skip_body: body = self.body for k, v in sorted(self.headers.items()): header = bytes_('%s: %s' % (k, v)) parts.append(header) if body: parts.extend([b'', body]) # HTTP clearly specifies CRLF return b'\r\n'.join(parts) def as_string(self, skip_body=False): warn_deprecation( "Please use req.as_bytes", '1.3', self._setattr_stacklevel ) return self.as_bytes(skip_body=skip_body) def as_text(self): bytes = self.as_bytes() return bytes.decode(self.charset) __str__ = as_text @classmethod def from_bytes(cls, b): """ Create a request from HTTP bytes data. If the bytes contain extra data after the request, raise a ValueError. """ f = io.BytesIO(b) r = cls.from_file(f) if f.tell() != len(b): raise ValueError("The string contains more data than expected") return r @classmethod def from_string(cls, b): warn_deprecation( "Please use req.from_bytes", '1.3', cls._setattr_stacklevel ) return cls.from_bytes(b) @classmethod def from_text(cls, s): b = bytes_(s, 'utf-8') return cls.from_bytes(b) @classmethod def from_file(cls, fp): """Read a request from a file-like object (it must implement ``.read(size)`` and ``.readline()``). It will read up to the end of the request, not the end of the file (unless the request is a POST or PUT and has no Content-Length, in that case, the entire file is read). This reads the request as represented by ``str(req)``; it may not read every valid HTTP request properly. """ start_line = fp.readline() is_text = isinstance(start_line, text_type) if is_text: crlf = '\r\n' colon = ':' else: crlf = b'\r\n' colon = b':' try: header = start_line.rstrip(crlf) method, resource, http_version = header.split(None, 2) method = native_(method, 'utf-8') resource = native_(resource, 'utf-8') http_version = native_(http_version, 'utf-8') except ValueError: raise ValueError('Bad HTTP request line: %r' % start_line) r = cls(environ_from_url(resource), http_version=http_version, method=method.upper() ) del r.environ['HTTP_HOST'] while 1: line = fp.readline() if not line.strip(): # end of headers break hname, hval = line.split(colon, 1) hname = native_(hname, 'utf-8') hval = native_(hval, 'utf-8').strip() if hname in r.headers: hval = r.headers[hname] + ', ' + hval r.headers[hname] = hval if r.method in ('PUT', 'POST'): clen = r.content_length if clen is None: body = fp.read() else: body = fp.read(clen) if is_text: body = bytes_(body, 'utf-8') r.body = body return r def call_application(self, application, catch_exc_info=False): """ Call the given WSGI application, returning ``(status_string, headerlist, app_iter)`` Be sure to call ``app_iter.close()`` if it's there. If catch_exc_info is true, then returns ``(status_string, headerlist, app_iter, exc_info)``, where the fourth item may be None, but won't be if there was an exception. If you don't do this and there was an exception, the exception will be raised directly. """ if self.is_body_seekable: self.body_file_raw.seek(0) captured = [] output = [] def start_response(status, headers, exc_info=None): if exc_info is not None and not catch_exc_info: reraise(exc_info) captured[:] = [status, headers, exc_info] return output.append app_iter = application(self.environ, start_response) if output or not captured: try: output.extend(app_iter) finally: if hasattr(app_iter, 'close'): app_iter.close() app_iter = output if catch_exc_info: return (captured[0], captured[1], app_iter, captured[2]) else: return (captured[0], captured[1], app_iter) # Will be filled in later: ResponseClass = None def send(self, application=None, catch_exc_info=False): """ Like ``.call_application(application)``, except returns a response object with ``.status``, ``.headers``, and ``.body`` attributes. This will use ``self.ResponseClass`` to figure out the class of the response object to return. If ``application`` is not given, this will send the request to ``self.make_default_send_app()`` """ if application is None: application = self.make_default_send_app() if catch_exc_info: status, headers, app_iter, exc_info = self.call_application( application, catch_exc_info=True) del exc_info else: status, headers, app_iter = self.call_application( application, catch_exc_info=False) return self.ResponseClass( status=status, headerlist=list(headers), app_iter=app_iter) get_response = send def make_default_send_app(self): global _client try: client = _client except NameError: from webob import client _client = client return client.send_request_app @classmethod def blank(cls, path, environ=None, base_url=None, headers=None, POST=None, **kw): """ Create a blank request environ (and Request wrapper) with the given path (path should be urlencoded), and any keys from environ. The path will become path_info, with any query string split off and used. All necessary keys will be added to the environ, but the values you pass in will take precedence. If you pass in base_url then wsgi.url_scheme, HTTP_HOST, and SCRIPT_NAME will be filled in from that value. Any extra keyword will be passed to ``__init__``. """ env = environ_from_url(path) if base_url: scheme, netloc, path, query, fragment = urlparse.urlsplit(base_url) if query or fragment: raise ValueError( "base_url (%r) cannot have a query or fragment" % base_url) if scheme: env['wsgi.url_scheme'] = scheme if netloc: if ':' not in netloc: if scheme == 'http': netloc += ':80' elif scheme == 'https': netloc += ':443' else: raise ValueError( "Unknown scheme: %r" % scheme) host, port = netloc.split(':', 1) env['SERVER_PORT'] = port env['SERVER_NAME'] = host env['HTTP_HOST'] = netloc if path: env['SCRIPT_NAME'] = url_unquote(path) if environ: env.update(environ) content_type = kw.get('content_type', env.get('CONTENT_TYPE')) if headers and 'Content-Type' in headers: content_type = headers['Content-Type'] if content_type is not None: kw['content_type'] = content_type environ_add_POST(env, POST, content_type=content_type) obj = cls(env, **kw) if headers is not None: obj.headers.update(headers) return obj class LegacyRequest(BaseRequest): uscript_name = upath_property('SCRIPT_NAME') upath_info = upath_property('PATH_INFO') def encget(self, key, default=NoDefault, encattr=None): val = self.environ.get(key, default) if val is NoDefault: raise KeyError(key) if val is default: return default return val class AdhocAttrMixin(object): _setattr_stacklevel = 3 def __setattr__(self, attr, value, DEFAULT=object()): if (getattr(self.__class__, attr, DEFAULT) is not DEFAULT or attr.startswith('_')): object.__setattr__(self, attr, value) else: self.environ.setdefault('webob.adhoc_attrs', {})[attr] = value def __getattr__(self, attr, DEFAULT=object()): try: return self.environ['webob.adhoc_attrs'][attr] except KeyError: raise AttributeError(attr) def __delattr__(self, attr, DEFAULT=object()): if getattr(self.__class__, attr, DEFAULT) is not DEFAULT: return object.__delattr__(self, attr) try: del self.environ['webob.adhoc_attrs'][attr] except KeyError: raise AttributeError(attr) class Request(AdhocAttrMixin, BaseRequest): """ The default request implementation """ def environ_from_url(path): if SCHEME_RE.search(path): scheme, netloc, path, qs, fragment = urlparse.urlsplit(path) if fragment: raise TypeError("Path cannot contain a fragment (%r)" % fragment) if qs: path += '?' + qs if ':' not in netloc: if scheme == 'http': netloc += ':80' elif scheme == 'https': netloc += ':443' else: raise TypeError("Unknown scheme: %r" % scheme) else: scheme = 'http' netloc = 'localhost:80' if path and '?' in path: path_info, query_string = path.split('?', 1) path_info = url_unquote(path_info) else: path_info = url_unquote(path) query_string = '' env = { 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'PATH_INFO': path_info or '', 'QUERY_STRING': query_string, 'SERVER_NAME': netloc.split(':')[0], 'SERVER_PORT': netloc.split(':')[1], 'HTTP_HOST': netloc, 'SERVER_PROTOCOL': 'HTTP/1.0', 'wsgi.version': (1, 0), 'wsgi.url_scheme': scheme, 'wsgi.input': io.BytesIO(), 'wsgi.errors': sys.stderr, 'wsgi.multithread': False, 'wsgi.multiprocess': False, 'wsgi.run_once': False, #'webob.is_body_seekable': True, } return env def environ_add_POST(env, data, content_type=None): if data is None: return elif isinstance(data, text_type): # pragma: no cover data = data.encode('ascii') if env['REQUEST_METHOD'] not in ('POST', 'PUT'): env['REQUEST_METHOD'] = 'POST' has_files = False if hasattr(data, 'items'): data = list(data.items()) for k, v in data: if isinstance(v, (tuple, list)): has_files = True break if content_type is None: if has_files: content_type = 'multipart/form-data' else: content_type = 'application/x-www-form-urlencoded' if content_type.startswith('multipart/form-data'): if not isinstance(data, bytes): content_type, data = _encode_multipart(data, content_type) elif content_type.startswith('application/x-www-form-urlencoded'): if has_files: raise ValueError('Submiting files is not allowed for' ' content type `%s`' % content_type) if not isinstance(data, bytes): data = url_encode(data) else: if not isinstance(data, bytes): raise ValueError('Please provide `POST` data as string' ' for content type `%s`' % content_type) data = bytes_(data, 'utf8') env['wsgi.input'] = io.BytesIO(data) env['webob.is_body_seekable'] = True env['CONTENT_LENGTH'] = str(len(data)) env['CONTENT_TYPE'] = content_type ######################### ## Helper classes and monkeypatching ######################### class DisconnectionError(IOError): pass class LimitedLengthFile(io.RawIOBase): def __init__(self, file, maxlen): self.file = file self.maxlen = maxlen self.remaining = maxlen def __repr__(self): return '<%s(%r, maxlen=%s)>' % ( self.__class__.__name__, self.file, self.maxlen ) def fileno(self): return self.file.fileno() @staticmethod def readable(): return True def readinto(self, buff): if not self.remaining: return 0 sz0 = min(len(buff), self.remaining) data = self.file.read(sz0) sz = len(data) self.remaining -= sz #if not data: if sz < sz0 and self.remaining: raise DisconnectionError( "The client disconnected while sending the POST/PUT body " + "(%d more bytes were expected)" % self.remaining ) buff[:sz] = data return sz def _cgi_FieldStorage__repr__patch(self): """ monkey patch for FieldStorage.__repr__ Unbelievably, the default __repr__ on FieldStorage reads the entire file content instead of being sane about it. This is a simple replacement that doesn't do that """ if self.file: return "FieldStorage(%r, %r)" % (self.name, self.filename) return "FieldStorage(%r, %r, %r)" % (self.name, self.filename, self.value) cgi.FieldStorage.__repr__ = _cgi_FieldStorage__repr__patch class FakeCGIBody(io.RawIOBase): def __init__(self, vars, content_type): if content_type.startswith('multipart/form-data'): if not _get_multipart_boundary(content_type): raise ValueError('Content-type: %r does not contain boundary' % content_type) self.vars = vars self.content_type = content_type self.file = None def __repr__(self): inner = repr(self.vars) if len(inner) > 20: inner = inner[:15] + '...' + inner[-5:] return '<%s at 0x%x viewing %s>' % ( self.__class__.__name__, abs(id(self)), inner) def fileno(self): return None @staticmethod def readable(): return True def readinto(self, buff): if self.file is None: if self.content_type.startswith( 'application/x-www-form-urlencoded'): data = '&'.join( '%s=%s' % (quote_plus(bytes_(k, 'utf8')), quote_plus(bytes_(v, 'utf8'))) for k,v in self.vars.items() ) self.file = io.BytesIO(bytes_(data)) elif self.content_type.startswith('multipart/form-data'): self.file = _encode_multipart( self.vars.items(), self.content_type, fout=io.BytesIO() )[1] self.file.seek(0) else: assert 0, ('Bad content type: %r' % self.content_type) return self.file.readinto(buff) def _get_multipart_boundary(ctype): m = re.search(r'boundary=([^ ]+)', ctype, re.I) if m: return native_(m.group(1).strip('"')) def _encode_multipart(vars, content_type, fout=None): """Encode a multipart request body into a string""" f = fout or io.BytesIO() w = f.write wt = lambda t: f.write(t.encode('utf8')) CRLF = b'\r\n' boundary = _get_multipart_boundary(content_type) if not boundary: boundary = native_(binascii.hexlify(os.urandom(10))) content_type += ('; boundary=%s' % boundary) for name, value in vars: w(b'--') wt(boundary) w(CRLF) assert name is not None, 'Value associated with no name: %r' % value wt('Content-Disposition: form-data; name="%s"' % name) filename = None if getattr(value, 'filename', None): filename = value.filename elif isinstance(value, (list, tuple)): filename, value = value if hasattr(value, 'read'): value = value.read() if filename is not None: wt('; filename="%s"' % filename) mime_type = mimetypes.guess_type(filename)[0] else: mime_type = None w(CRLF) # TODO: should handle value.disposition_options if getattr(value, 'type', None): wt('Content-type: %s' % value.type) if value.type_options: for ct_name, ct_value in sorted(value.type_options.items()): wt('; %s="%s"' % (ct_name, ct_value)) w(CRLF) elif mime_type: wt('Content-type: %s' % mime_type) w(CRLF) w(CRLF) if hasattr(value, 'value'): value = value.value if isinstance(value, bytes): w(value) else: wt(value) w(CRLF) wt('--%s--' % boundary) if fout: return content_type, fout else: return content_type, f.getvalue() def detect_charset(ctype): m = CHARSET_RE.search(ctype) if m: return m.group(1).strip('"').strip() def _is_utf8(charset): if not charset: return True else: return charset.lower().replace('-', '') == 'utf8' class Transcoder(object): def __init__(self, charset, errors='strict'): self.charset = charset # source charset self.errors = errors # unicode errors self._trans = lambda b: b.decode(charset, errors).encode('utf8') def transcode_query(self, q): if PY3: # pragma: no cover q_orig = q if '=' not in q: # this doesn't look like a form submission return q_orig q = list(parse_qsl_text(q, self.charset)) return url_encode(q) else: q_orig = q if '=' not in q: # this doesn't look like a form submission return q_orig q = urlparse.parse_qsl(q, self.charset) t = self._trans q = [(t(k), t(v)) for k,v in q] return url_encode(q) def transcode_fs(self, fs, content_type): # transcode FieldStorage if PY3: # pragma: no cover decode = lambda b: b else: decode = lambda b: b.decode(self.charset, self.errors) data = [] for field in fs.list or (): field.name = decode(field.name) if field.filename: field.filename = decode(field.filename) data.append((field.name, field)) else: data.append((field.name, decode(field.value))) # TODO: transcode big requests to temp file content_type, fout = _encode_multipart( data, content_type, fout=io.BytesIO() ) return fout # TODO: remove in 1.4 for _name in 'GET POST params cookies'.split(): _str_name = 'str_'+_name _prop = deprecated_property( None, _str_name, "disabled starting WebOb 1.2, use %s instead" % _name, '1.2') setattr(BaseRequest, _str_name, _prop) WebOb-1.3.1/webob/response.py0000664000175000017500000012347612127135226016634 0ustar chrismchrism00000000000000from base64 import b64encode from datetime import ( datetime, timedelta, ) from hashlib import md5 import re import struct import zlib try: import simplejson as json except ImportError: import json from webob.byterange import ContentRange from webob.cachecontrol import ( CacheControl, serialize_cache_control, ) from webob.compat import ( PY3, bytes_, native_, text_type, url_quote, urlparse, ) from webob.cookies import ( Cookie, Morsel, ) from webob.datetime_utils import ( parse_date_delta, serialize_date_delta, timedelta_to_seconds, ) from webob.descriptors import ( CHARSET_RE, SCHEME_RE, converter, date_header, header_getter, list_header, parse_auth, parse_content_range, parse_etag_response, parse_int, parse_int_safe, serialize_auth, serialize_content_range, serialize_etag_response, serialize_int, ) from webob.headers import ResponseHeaders from webob.request import BaseRequest from webob.util import status_reasons, status_generic_reasons __all__ = ['Response'] _PARAM_RE = re.compile(r'([a-z0-9]+)=(?:"([^"]*)"|([a-z0-9_.-]*))', re.I) _OK_PARAM_RE = re.compile(r'^[a-z0-9_.-]+$', re.I) _gzip_header = b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff' class Response(object): """ Represents a WSGI response """ default_content_type = 'text/html' default_charset = 'UTF-8' # TODO: deprecate unicode_errors = 'strict' # TODO: deprecate (why would response body have errors?) default_conditional_response = False request = None environ = None # # __init__, from_file, copy # def __init__(self, body=None, status=None, headerlist=None, app_iter=None, content_type=None, conditional_response=None, **kw): if app_iter is None and body is None and ('json_body' in kw or 'json' in kw): if 'json_body' in kw: json_body = kw.pop('json_body') else: json_body = kw.pop('json') body = json.dumps(json_body, separators=(',', ':')) if content_type is None: content_type = 'application/json' if app_iter is None: if body is None: body = b'' elif body is not None: raise TypeError( "You may only give one of the body and app_iter arguments") if status is None: self._status = '200 OK' else: self.status = status if headerlist is None: self._headerlist = [] else: self._headerlist = headerlist self._headers = None if content_type is None: content_type = self.default_content_type charset = None if 'charset' in kw: charset = kw.pop('charset') elif self.default_charset: if (content_type and 'charset=' not in content_type and (content_type == 'text/html' or content_type.startswith('text/') or content_type.startswith('application/xml') or content_type.startswith('application/json') or (content_type.startswith('application/') and (content_type.endswith('+xml') or content_type.endswith('+json'))))): charset = self.default_charset if content_type and charset: content_type += '; charset=' + charset elif self._headerlist and charset: self.charset = charset if not self._headerlist and content_type: self._headerlist.append(('Content-Type', content_type)) if conditional_response is None: self.conditional_response = self.default_conditional_response else: self.conditional_response = bool(conditional_response) if app_iter is None: if isinstance(body, text_type): if charset is None: raise TypeError( "You cannot set the body to a text value without a " "charset") body = body.encode(charset) app_iter = [body] if headerlist is None: self._headerlist.append(('Content-Length', str(len(body)))) else: self.headers['Content-Length'] = str(len(body)) self._app_iter = app_iter for name, value in kw.items(): if not hasattr(self.__class__, name): # Not a basic attribute raise TypeError( "Unexpected keyword: %s=%r" % (name, value)) setattr(self, name, value) @classmethod def from_file(cls, fp): """Reads a response from a file-like object (it must implement ``.read(size)`` and ``.readline()``). It will read up to the end of the response, not the end of the file. This reads the response as represented by ``str(resp)``; it may not read every valid HTTP response properly. Responses must have a ``Content-Length``""" headerlist = [] status = fp.readline().strip() is_text = isinstance(status, text_type) if is_text: _colon = ':' else: _colon = b':' while 1: line = fp.readline().strip() if not line: # end of headers break try: header_name, value = line.split(_colon, 1) except ValueError: raise ValueError('Bad header line: %r' % line) value = value.strip() if not is_text: header_name = header_name.decode('utf-8') value = value.decode('utf-8') headerlist.append((header_name, value)) r = cls( status=status, headerlist=headerlist, app_iter=(), ) body = fp.read(r.content_length or 0) if is_text: r.text = body else: r.body = body return r def copy(self): """Makes a copy of the response""" # we need to do this for app_iter to be reusable app_iter = list(self._app_iter) iter_close(self._app_iter) # and this to make sure app_iter instances are different self._app_iter = list(app_iter) return self.__class__( content_type=False, status=self._status, headerlist=self._headerlist[:], app_iter=app_iter, conditional_response=self.conditional_response) # # __repr__, __str__ # def __repr__(self): return '<%s at 0x%x %s>' % (self.__class__.__name__, abs(id(self)), self.status) def __str__(self, skip_body=False): parts = [self.status] if not skip_body: # Force enumeration of the body (to set content-length) self.body parts += map('%s: %s'.__mod__, self.headerlist) if not skip_body and self.body: parts += ['', self.text if PY3 else self.body] return '\n'.join(parts) # # status, status_code/status_int # def _status__get(self): """ The status string """ return self._status def _status__set(self, value): if isinstance(value, int): self.status_code = value return if PY3: # pragma: no cover if isinstance(value, bytes): value = value.decode('ascii') elif isinstance(value, text_type): value = value.encode('ascii') if not isinstance(value, str): raise TypeError( "You must set status to a string or integer (not %s)" % type(value)) if ' ' not in value: try: value += ' ' + status_reasons[int(value)] except KeyError: value += ' ' + status_generic_reasons[int(value) // 100] self._status = value status = property(_status__get, _status__set, doc=_status__get.__doc__) def _status_code__get(self): """ The status as an integer """ return int(self._status.split()[0]) def _status_code__set(self, code): try: self._status = '%d %s' % (code, status_reasons[code]) except KeyError: self._status = '%d %s' % (code, status_generic_reasons[code // 100]) status_code = status_int = property(_status_code__get, _status_code__set, doc=_status_code__get.__doc__) # # headerslist, headers # def _headerlist__get(self): """ The list of response headers """ return self._headerlist def _headerlist__set(self, value): self._headers = None if not isinstance(value, list): if hasattr(value, 'items'): value = value.items() value = list(value) self._headerlist = value def _headerlist__del(self): self.headerlist = [] headerlist = property(_headerlist__get, _headerlist__set, _headerlist__del, doc=_headerlist__get.__doc__) def _headers__get(self): """ The headers in a dictionary-like object """ if self._headers is None: self._headers = ResponseHeaders.view_list(self.headerlist) return self._headers def _headers__set(self, value): if hasattr(value, 'items'): value = value.items() self.headerlist = value self._headers = None headers = property(_headers__get, _headers__set, doc=_headers__get.__doc__) # # body # def _body__get(self): """ The body of the response, as a ``str``. This will read in the entire app_iter if necessary. """ app_iter = self._app_iter # try: # if len(app_iter) == 1: # return app_iter[0] # except: # pass if isinstance(app_iter, list) and len(app_iter) == 1: return app_iter[0] if app_iter is None: raise AttributeError("No body has been set") try: body = b''.join(app_iter) finally: iter_close(app_iter) if isinstance(body, text_type): raise _error_unicode_in_app_iter(app_iter, body) self._app_iter = [body] if len(body) == 0: # if body-length is zero, we assume it's a HEAD response and # leave content_length alone pass # pragma: no cover (no idea why necessary, it's hit) elif self.content_length is None: self.content_length = len(body) elif self.content_length != len(body): raise AssertionError( "Content-Length is different from actual app_iter length " "(%r!=%r)" % (self.content_length, len(body)) ) return body def _body__set(self, value=b''): if not isinstance(value, bytes): if isinstance(value, text_type): msg = ("You cannot set Response.body to a text object " "(use Response.text)") else: msg = ("You can only set the body to a binary type (not %s)" % type(value)) raise TypeError(msg) if self._app_iter is not None: self.content_md5 = None self._app_iter = [value] self.content_length = len(value) # def _body__del(self): # self.body = '' # #self.content_length = None body = property(_body__get, _body__set, _body__set) def _json_body__get(self): """Access the body of the response as JSON""" # Note: UTF-8 is a content-type specific default for JSON: return json.loads(self.body.decode(self.charset or 'UTF-8')) def _json_body__set(self, value): self.body = json.dumps(value, separators=(',', ':')).encode(self.charset or 'UTF-8') def _json_body__del(self): del self.body json = json_body = property(_json_body__get, _json_body__set, _json_body__del) # # text, unicode_body, ubody # def _text__get(self): """ Get/set the text value of the body (using the charset of the Content-Type) """ if not self.charset: raise AttributeError( "You cannot access Response.text unless charset is set") body = self.body return body.decode(self.charset, self.unicode_errors) def _text__set(self, value): if not self.charset: raise AttributeError( "You cannot access Response.text unless charset is set") if not isinstance(value, text_type): raise TypeError( "You can only set Response.text to a unicode string " "(not %s)" % type(value)) self.body = value.encode(self.charset) def _text__del(self): del self.body text = property(_text__get, _text__set, _text__del, doc=_text__get.__doc__) unicode_body = ubody = property(_text__get, _text__set, _text__del, "Deprecated alias for .text") # # body_file, write(text) # def _body_file__get(self): """ A file-like object that can be used to write to the body. If you passed in a list app_iter, that app_iter will be modified by writes. """ return ResponseBodyFile(self) def _body_file__set(self, file): self.app_iter = iter_file(file) def _body_file__del(self): del self.body body_file = property(_body_file__get, _body_file__set, _body_file__del, doc=_body_file__get.__doc__) def write(self, text): if not isinstance(text, bytes): if not isinstance(text, text_type): msg = "You can only write str to a Response.body_file, not %s" raise TypeError(msg % type(text)) if not self.charset: msg = ("You can only write text to Response if charset has " "been set") raise TypeError(msg) text = text.encode(self.charset) app_iter = self._app_iter if not isinstance(app_iter, list): try: new_app_iter = self._app_iter = list(app_iter) finally: iter_close(app_iter) app_iter = new_app_iter self.content_length = sum(len(chunk) for chunk in app_iter) app_iter.append(text) if self.content_length is not None: self.content_length += len(text) # # app_iter # def _app_iter__get(self): """ Returns the app_iter of the response. If body was set, this will create an app_iter from that body (a single-item list) """ return self._app_iter def _app_iter__set(self, value): if self._app_iter is not None: # Undo the automatically-set content-length self.content_length = None self.content_md5 = None self._app_iter = value def _app_iter__del(self): self._app_iter = [] self.content_length = None app_iter = property(_app_iter__get, _app_iter__set, _app_iter__del, doc=_app_iter__get.__doc__) # # headers attrs # allow = list_header('Allow', '14.7') # TODO: (maybe) support response.vary += 'something' # TODO: same thing for all listy headers vary = list_header('Vary', '14.44') content_length = converter( header_getter('Content-Length', '14.17'), parse_int, serialize_int, 'int') content_encoding = header_getter('Content-Encoding', '14.11') content_language = list_header('Content-Language', '14.12') content_location = header_getter('Content-Location', '14.14') content_md5 = header_getter('Content-MD5', '14.14') content_disposition = header_getter('Content-Disposition', '19.5.1') accept_ranges = header_getter('Accept-Ranges', '14.5') content_range = converter( header_getter('Content-Range', '14.16'), parse_content_range, serialize_content_range, 'ContentRange object') date = date_header('Date', '14.18') expires = date_header('Expires', '14.21') last_modified = date_header('Last-Modified', '14.29') _etag_raw = header_getter('ETag', '14.19') etag = converter(_etag_raw, parse_etag_response, serialize_etag_response, 'Entity tag' ) @property def etag_strong(self): return parse_etag_response(self._etag_raw, strong=True) location = header_getter('Location', '14.30') pragma = header_getter('Pragma', '14.32') age = converter( header_getter('Age', '14.6'), parse_int_safe, serialize_int, 'int') retry_after = converter( header_getter('Retry-After', '14.37'), parse_date_delta, serialize_date_delta, 'HTTP date or delta seconds') server = header_getter('Server', '14.38') # TODO: the standard allows this to be a list of challenges www_authenticate = converter( header_getter('WWW-Authenticate', '14.47'), parse_auth, serialize_auth, ) # # charset # def _charset__get(self): """ Get/set the charset (in the Content-Type) """ header = self.headers.get('Content-Type') if not header: return None match = CHARSET_RE.search(header) if match: return match.group(1) return None def _charset__set(self, charset): if charset is None: del self.charset return header = self.headers.pop('Content-Type', None) if header is None: raise AttributeError("You cannot set the charset when no " "content-type is defined") match = CHARSET_RE.search(header) if match: header = header[:match.start()] + header[match.end():] header += '; charset=%s' % charset self.headers['Content-Type'] = header def _charset__del(self): header = self.headers.pop('Content-Type', None) if header is None: # Don't need to remove anything return match = CHARSET_RE.search(header) if match: header = header[:match.start()] + header[match.end():] self.headers['Content-Type'] = header charset = property(_charset__get, _charset__set, _charset__del, doc=_charset__get.__doc__) # # content_type # def _content_type__get(self): """ Get/set the Content-Type header (or None), *without* the charset or any parameters. If you include parameters (or ``;`` at all) when setting the content_type, any existing parameters will be deleted; otherwise they will be preserved. """ header = self.headers.get('Content-Type') if not header: return None return header.split(';', 1)[0] def _content_type__set(self, value): if not value: self._content_type__del() return if ';' not in value: header = self.headers.get('Content-Type', '') if ';' in header: params = header.split(';', 1)[1] value += ';' + params self.headers['Content-Type'] = value def _content_type__del(self): self.headers.pop('Content-Type', None) content_type = property(_content_type__get, _content_type__set, _content_type__del, doc=_content_type__get.__doc__) # # content_type_params # def _content_type_params__get(self): """ A dictionary of all the parameters in the content type. (This is not a view, set to change, modifications of the dict would not be applied otherwise) """ params = self.headers.get('Content-Type', '') if ';' not in params: return {} params = params.split(';', 1)[1] result = {} for match in _PARAM_RE.finditer(params): result[match.group(1)] = match.group(2) or match.group(3) or '' return result def _content_type_params__set(self, value_dict): if not value_dict: del self.content_type_params return params = [] for k, v in sorted(value_dict.items()): if not _OK_PARAM_RE.search(v): v = '"%s"' % v.replace('"', '\\"') params.append('; %s=%s' % (k, v)) ct = self.headers.pop('Content-Type', '').split(';', 1)[0] ct += ''.join(params) self.headers['Content-Type'] = ct def _content_type_params__del(self): self.headers['Content-Type'] = self.headers.get( 'Content-Type', '').split(';', 1)[0] content_type_params = property( _content_type_params__get, _content_type_params__set, _content_type_params__del, _content_type_params__get.__doc__ ) # # set_cookie, unset_cookie, delete_cookie, merge_cookies # def set_cookie(self, key, value='', max_age=None, path='/', domain=None, secure=False, httponly=False, comment=None, expires=None, overwrite=False): """ Set (add) a cookie for the response. Arguments are: ``key`` The cookie name. ``value`` The cookie value, which should be a string or ``None``. If ``value`` is ``None``, it's equivalent to calling the :meth:`webob.response.Response.unset_cookie` method for this cookie key (it effectively deletes the cookie on the client). ``max_age`` An integer representing a number of seconds or ``None``. If this value is an integer, it is used as the ``Max-Age`` of the generated cookie. If ``expires`` is not passed and this value is an integer, the ``max_age`` value will also influence the ``Expires`` value of the cookie (``Expires`` will be set to now + max_age). If this value is ``None``, the cookie will not have a ``Max-Age`` value (unless ``expires`` is also sent). ``path`` A string representing the cookie ``Path`` value. It defaults to ``/``. ``domain`` A string representing the cookie ``Domain``, or ``None``. If domain is ``None``, no ``Domain`` value will be sent in the cookie. ``secure`` A boolean. If it's ``True``, the ``secure`` flag will be sent in the cookie, if it's ``False``, the ``secure`` flag will not be sent in the cookie. ``httponly`` A boolean. If it's ``True``, the ``HttpOnly`` flag will be sent in the cookie, if it's ``False``, the ``HttpOnly`` flag will not be sent in the cookie. ``comment`` A string representing the cookie ``Comment`` value, or ``None``. If ``comment`` is ``None``, no ``Comment`` value will be sent in the cookie. ``expires`` A ``datetime.timedelta`` object representing an amount of time or the value ``None``. A non-``None`` value is used to generate the ``Expires`` value of the generated cookie. If ``max_age`` is not passed, but this value is not ``None``, it will influence the ``Max-Age`` header (``Max-Age`` will be 'expires_value - datetime.utcnow()'). If this value is ``None``, the ``Expires`` cookie value will be unset (unless ``max_age`` is also passed). ``overwrite`` If this key is ``True``, before setting the cookie, unset any existing cookie. """ if overwrite: self.unset_cookie(key, strict=False) if value is None: # delete the cookie from the client value = '' max_age = 0 expires = timedelta(days=-5) elif expires is None and max_age is not None: if isinstance(max_age, int): max_age = timedelta(seconds=max_age) expires = datetime.utcnow() + max_age elif max_age is None and expires is not None: max_age = expires - datetime.utcnow() value = bytes_(value, 'utf8') key = bytes_(key, 'utf8') m = Morsel(key, value) m.path = bytes_(path, 'utf8') m.domain = bytes_(domain, 'utf8') m.comment = bytes_(comment, 'utf8') m.expires = expires m.max_age = max_age m.secure = secure m.httponly = httponly self.headerlist.append(('Set-Cookie', m.serialize())) def delete_cookie(self, key, path='/', domain=None): """ Delete a cookie from the client. Note that path and domain must match how the cookie was originally set. This sets the cookie to the empty string, and max_age=0 so that it should expire immediately. """ self.set_cookie(key, None, path=path, domain=domain) def unset_cookie(self, key, strict=True): """ Unset a cookie with the given name (remove it from the response). """ existing = self.headers.getall('Set-Cookie') if not existing and not strict: return cookies = Cookie() for header in existing: cookies.load(header) if isinstance(key, text_type): key = key.encode('utf8') if key in cookies: del cookies[key] del self.headers['Set-Cookie'] for m in cookies.values(): self.headerlist.append(('Set-Cookie', m.serialize())) elif strict: raise KeyError("No cookie has been set with the name %r" % key) def merge_cookies(self, resp): """Merge the cookies that were set on this response with the given `resp` object (which can be any WSGI application). If the `resp` is a :class:`webob.Response` object, then the other object will be modified in-place. """ if not self.headers.get('Set-Cookie'): return resp if isinstance(resp, Response): for header in self.headers.getall('Set-Cookie'): resp.headers.add('Set-Cookie', header) return resp else: c_headers = [h for h in self.headerlist if h[0].lower() == 'set-cookie'] def repl_app(environ, start_response): def repl_start_response(status, headers, exc_info=None): return start_response(status, headers+c_headers, exc_info=exc_info) return resp(environ, repl_start_response) return repl_app # # cache_control # _cache_control_obj = None def _cache_control__get(self): """ Get/set/modify the Cache-Control header (`HTTP spec section 14.9 `_) """ value = self.headers.get('cache-control', '') if self._cache_control_obj is None: self._cache_control_obj = CacheControl.parse( value, updates_to=self._update_cache_control, type='response') self._cache_control_obj.header_value = value if self._cache_control_obj.header_value != value: new_obj = CacheControl.parse(value, type='response') self._cache_control_obj.properties.clear() self._cache_control_obj.properties.update(new_obj.properties) self._cache_control_obj.header_value = value return self._cache_control_obj def _cache_control__set(self, value): # This actually becomes a copy if not value: value = "" if isinstance(value, dict): value = CacheControl(value, 'response') if isinstance(value, text_type): value = str(value) if isinstance(value, str): if self._cache_control_obj is None: self.headers['Cache-Control'] = value return value = CacheControl.parse(value, 'response') cache = self.cache_control cache.properties.clear() cache.properties.update(value.properties) def _cache_control__del(self): self.cache_control = {} def _update_cache_control(self, prop_dict): value = serialize_cache_control(prop_dict) if not value: if 'Cache-Control' in self.headers: del self.headers['Cache-Control'] else: self.headers['Cache-Control'] = value cache_control = property( _cache_control__get, _cache_control__set, _cache_control__del, doc=_cache_control__get.__doc__) # # cache_expires # def _cache_expires(self, seconds=0, **kw): """ Set expiration on this request. This sets the response to expire in the given seconds, and any other attributes are used for cache_control (e.g., private=True, etc). """ if seconds is True: seconds = 0 elif isinstance(seconds, timedelta): seconds = timedelta_to_seconds(seconds) cache_control = self.cache_control if seconds is None: pass elif not seconds: # To really expire something, you have to force a # bunch of these cache control attributes, and IE may # not pay attention to those still so we also set # Expires. cache_control.no_store = True cache_control.no_cache = True cache_control.must_revalidate = True cache_control.max_age = 0 cache_control.post_check = 0 cache_control.pre_check = 0 self.expires = datetime.utcnow() if 'last-modified' not in self.headers: self.last_modified = datetime.utcnow() self.pragma = 'no-cache' else: cache_control.properties.clear() cache_control.max_age = seconds self.expires = datetime.utcnow() + timedelta(seconds=seconds) self.pragma = None for name, value in kw.items(): setattr(cache_control, name, value) cache_expires = property(lambda self: self._cache_expires, _cache_expires) # # encode_content, decode_content, md5_etag # def encode_content(self, encoding='gzip', lazy=False): """ Encode the content with the given encoding (only gzip and identity are supported). """ assert encoding in ('identity', 'gzip'), \ "Unknown encoding: %r" % encoding if encoding == 'identity': self.decode_content() return if self.content_encoding == 'gzip': return if lazy: self.app_iter = gzip_app_iter(self._app_iter) self.content_length = None else: self.app_iter = list(gzip_app_iter(self._app_iter)) self.content_length = sum(map(len, self._app_iter)) self.content_encoding = 'gzip' def decode_content(self): content_encoding = self.content_encoding or 'identity' if content_encoding == 'identity': return if content_encoding not in ('gzip', 'deflate'): raise ValueError( "I don't know how to decode the content %s" % content_encoding) if content_encoding == 'gzip': from gzip import GzipFile from io import BytesIO gzip_f = GzipFile(filename='', mode='r', fileobj=BytesIO(self.body)) self.body = gzip_f.read() self.content_encoding = None gzip_f.close() else: # Weird feature: http://bugs.python.org/issue5784 self.body = zlib.decompress(self.body, -15) self.content_encoding = None def md5_etag(self, body=None, set_content_md5=False): """ Generate an etag for the response object using an MD5 hash of the body (the body parameter, or ``self.body`` if not given) Sets ``self.etag`` If ``set_content_md5`` is True sets ``self.content_md5`` as well """ if body is None: body = self.body md5_digest = md5(body).digest() md5_digest = b64encode(md5_digest) md5_digest = md5_digest.replace(b'\n', b'') md5_digest = native_(md5_digest) self.etag = md5_digest.strip('=') if set_content_md5: self.content_md5 = md5_digest # # __call__, conditional_response_app # def __call__(self, environ, start_response): """ WSGI application interface """ if self.conditional_response: return self.conditional_response_app(environ, start_response) headerlist = self._abs_headerlist(environ) start_response(self.status, headerlist) if environ['REQUEST_METHOD'] == 'HEAD': # Special case here... return EmptyResponse(self._app_iter) return self._app_iter def _abs_headerlist(self, environ): """Returns a headerlist, with the Location header possibly made absolute given the request environ. """ headerlist = list(self.headerlist) for i, (name, value) in enumerate(headerlist): if name.lower() == 'location': if SCHEME_RE.search(value): break new_location = urlparse.urljoin(_request_uri(environ), value) headerlist[i] = (name, new_location) break return headerlist _safe_methods = ('GET', 'HEAD') def conditional_response_app(self, environ, start_response): """ Like the normal __call__ interface, but checks conditional headers: * If-Modified-Since (304 Not Modified; only on GET, HEAD) * If-None-Match (304 Not Modified; only on GET, HEAD) * Range (406 Partial Content; only on GET, HEAD) """ req = BaseRequest(environ) headerlist = self._abs_headerlist(environ) method = environ.get('REQUEST_METHOD', 'GET') if method in self._safe_methods: status304 = False if req.if_none_match and self.etag: status304 = self.etag in req.if_none_match elif req.if_modified_since and self.last_modified: status304 = self.last_modified <= req.if_modified_since if status304: start_response('304 Not Modified', filter_headers(headerlist)) return EmptyResponse(self._app_iter) if (req.range and self in req.if_range and self.content_range is None and method in ('HEAD', 'GET') and self.status_code == 200 and self.content_length is not None ): content_range = req.range.content_range(self.content_length) if content_range is None: iter_close(self._app_iter) body = bytes_("Requested range not satisfiable: %s" % req.range) headerlist = [ ('Content-Length', str(len(body))), ('Content-Range', str(ContentRange(None, None, self.content_length))), ('Content-Type', 'text/plain'), ] + filter_headers(headerlist) start_response('416 Requested Range Not Satisfiable', headerlist) if method == 'HEAD': return () return [body] else: app_iter = self.app_iter_range(content_range.start, content_range.stop) if app_iter is not None: # the following should be guaranteed by # Range.range_for_length(length) assert content_range.start is not None headerlist = [ ('Content-Length', str(content_range.stop - content_range.start)), ('Content-Range', str(content_range)), ] + filter_headers(headerlist, ('content-length',)) start_response('206 Partial Content', headerlist) if method == 'HEAD': return EmptyResponse(app_iter) return app_iter start_response(self.status, headerlist) if method == 'HEAD': return EmptyResponse(self._app_iter) return self._app_iter def app_iter_range(self, start, stop): """ Return a new app_iter built from the response app_iter, that serves up only the given ``start:stop`` range. """ app_iter = self._app_iter if hasattr(app_iter, 'app_iter_range'): return app_iter.app_iter_range(start, stop) return AppIterRange(app_iter, start, stop) def filter_headers(hlist, remove_headers=('content-length', 'content-type')): return [h for h in hlist if (h[0].lower() not in remove_headers)] def iter_file(file, block_size=1<<18): # 256Kb while True: data = file.read(block_size) if not data: break yield data class ResponseBodyFile(object): mode = 'wb' closed = False def __init__(self, response): self.response = response self.write = response.write def __repr__(self): return '' % self.response encoding = property( lambda self: self.response.charset, doc="The encoding of the file (inherited from response.charset)" ) def writelines(self, seq): for item in seq: self.write(item) def close(self): raise NotImplementedError("Response bodies cannot be closed") def flush(self): pass class AppIterRange(object): """ Wraps an app_iter, returning just a range of bytes """ def __init__(self, app_iter, start, stop): assert start >= 0, "Bad start: %r" % start assert stop is None or (stop >= 0 and stop >= start), ( "Bad stop: %r" % stop) self.app_iter = iter(app_iter) self._pos = 0 # position in app_iter self.start = start self.stop = stop def __iter__(self): return self def _skip_start(self): start, stop = self.start, self.stop for chunk in self.app_iter: self._pos += len(chunk) if self._pos < start: continue elif self._pos == start: return b'' else: chunk = chunk[start-self._pos:] if stop is not None and self._pos > stop: chunk = chunk[:stop-self._pos] assert len(chunk) == stop - start return chunk else: raise StopIteration() def next(self): if self._pos < self.start: # need to skip some leading bytes return self._skip_start() stop = self.stop if stop is not None and self._pos >= stop: raise StopIteration chunk = next(self.app_iter) self._pos += len(chunk) if stop is None or self._pos <= stop: return chunk else: return chunk[:stop-self._pos] __next__ = next # py3 def close(self): iter_close(self.app_iter) class EmptyResponse(object): """An empty WSGI response. An iterator that immediately stops. Optionally provides a close method to close an underlying app_iter it replaces. """ def __init__(self, app_iter=None): if app_iter and hasattr(app_iter, 'close'): self.close = app_iter.close def __iter__(self): return self def __len__(self): return 0 def next(self): raise StopIteration() __next__ = next # py3 def _request_uri(environ): """Like wsgiref.url.request_uri, except eliminates :80 ports Return the full request URI""" url = environ['wsgi.url_scheme']+'://' if environ.get('HTTP_HOST'): url += environ['HTTP_HOST'] else: url += environ['SERVER_NAME'] + ':' + environ['SERVER_PORT'] if url.endswith(':80') and environ['wsgi.url_scheme'] == 'http': url = url[:-3] elif url.endswith(':443') and environ['wsgi.url_scheme'] == 'https': url = url[:-4] if PY3: # pragma: no cover script_name = bytes_(environ.get('SCRIPT_NAME', '/'), 'latin-1') path_info = bytes_(environ.get('PATH_INFO', ''), 'latin-1') else: script_name = environ.get('SCRIPT_NAME', '/') path_info = environ.get('PATH_INFO', '') url += url_quote(script_name) qpath_info = url_quote(path_info) if not 'SCRIPT_NAME' in environ: url += qpath_info[1:] else: url += qpath_info return url def iter_close(iter): if hasattr(iter, 'close'): iter.close() def gzip_app_iter(app_iter): size = 0 crc = zlib.crc32(b"") & 0xffffffff compress = zlib.compressobj(9, zlib.DEFLATED, -zlib.MAX_WBITS, zlib.DEF_MEM_LEVEL, 0) yield _gzip_header for item in app_iter: size += len(item) crc = zlib.crc32(item, crc) & 0xffffffff # The compress function may return zero length bytes if the input is # small enough; it buffers the input for the next iteration or for a # flush. result = compress.compress(item) if result: yield result # Similarly, flush may also not yield a value. result = compress.flush() if result: yield result yield struct.pack("<2L", crc, size & 0xffffffff) def _error_unicode_in_app_iter(app_iter, body): app_iter_repr = repr(app_iter) if len(app_iter_repr) > 50: app_iter_repr = ( app_iter_repr[:30] + '...' + app_iter_repr[-10:]) raise TypeError( 'An item of the app_iter (%s) was text, causing a ' 'text body: %r' % (app_iter_repr, body)) WebOb-1.3.1/webob/util.py0000664000175000017500000001052712250507612015742 0ustar chrismchrism00000000000000import warnings from webob.compat import ( escape, string_types, text_, text_type, ) from webob.headers import _trans_key def html_escape(s): """HTML-escape a string or object This converts any non-string objects passed into it to strings (actually, using ``unicode()``). All values returned are non-unicode strings (using ``&#num;`` entities for all non-ASCII characters). None is treated specially, and returns the empty string. """ if s is None: return '' __html__ = getattr(s, '__html__', None) if __html__ is not None and callable(__html__): return s.__html__() if not isinstance(s, string_types): __unicode__ = getattr(s, '__unicode__', None) if __unicode__ is not None and callable(__unicode__): s = s.__unicode__() else: s = str(s) s = escape(s, True) if isinstance(s, text_type): s = s.encode('ascii', 'xmlcharrefreplace') return text_(s) def header_docstring(header, rfc_section): if header.isupper(): header = _trans_key(header) major_section = rfc_section.split('.')[0] link = 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec%s.html#sec%s' % ( major_section, rfc_section) return "Gets and sets the ``%s`` header (`HTTP spec section %s <%s>`_)." % ( header, rfc_section, link) def warn_deprecation(text, version, stacklevel): # pragma: no cover # version specifies when to start raising exceptions instead of warnings if version == '1.2': raise DeprecationWarning(text) elif version == '1.3': cls = DeprecationWarning else: cls = DeprecationWarning warnings.warn("Unknown warn_deprecation version arg: %r" % version, RuntimeWarning, stacklevel=1 ) warnings.warn(text, cls, stacklevel=stacklevel+1) status_reasons = { # Status Codes # Informational 100: 'Continue', 101: 'Switching Protocols', 102: 'Processing', # Successful 200: 'OK', 201: 'Created', 202: 'Accepted', 203: 'Non-Authoritative Information', 204: 'No Content', 205: 'Reset Content', 206: 'Partial Content', 207: 'Multi Status', 226: 'IM Used', # Redirection 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', 303: 'See Other', 304: 'Not Modified', 305: 'Use Proxy', 307: 'Temporary Redirect', # Client Error 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', 403: 'Forbidden', 404: 'Not Found', 405: 'Method Not Allowed', 406: 'Not Acceptable', 407: 'Proxy Authentication Required', 408: 'Request Timeout', 409: 'Conflict', 410: 'Gone', 411: 'Length Required', 412: 'Precondition Failed', 413: 'Request Entity Too Large', 414: 'Request URI Too Long', 415: 'Unsupported Media Type', 416: 'Requested Range Not Satisfiable', 417: 'Expectation Failed', 418: "I'm a teapot", 422: 'Unprocessable Entity', 423: 'Locked', 424: 'Failed Dependency', 426: 'Upgrade Required', 428: 'Precondition Required', 429: 'Too Many Requests', 451: 'Unavailable for Legal Reasons', 431: 'Request Header Fields Too Large', # Server Error 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', 503: 'Service Unavailable', 504: 'Gateway Timeout', 505: 'HTTP Version Not Supported', 507: 'Insufficient Storage', 510: 'Not Extended', 511: 'Network Authentication Required', } # generic class responses as per RFC2616 status_generic_reasons = { 1: 'Continue', 2: 'Success', 3: 'Multiple Choices', 4: 'Unknown Client Error', 5: 'Unknown Server Error', } def strings_differ(string1, string2): """Check whether two strings differ while avoiding timing attacks. This function returns True if the given strings differ and False if they are equal. It's careful not to leak information about *where* they differ as a result of its running time, which can be very important to avoid certain timing-related crypto attacks: http://seb.dbzteam.org/crypto/python-oauth-timing-hmac.pdf """ if len(string1) != len(string2): return True invalid_bits = 0 for a, b in zip(string1, string2): invalid_bits += a != b return invalid_bits != 0 WebOb-1.3.1/webob/client.py0000664000175000017500000001537112123261311016235 0ustar chrismchrism00000000000000import errno import sys import re try: import httplib except ImportError: # pragma: no cover import http.client as httplib from webob.compat import url_quote import socket from webob import exc from webob.compat import PY3 __all__ = ['send_request_app', 'SendRequest'] class SendRequest: """ Sends the request, as described by the environ, over actual HTTP. All controls about how it is sent are contained in the request environ itself. This connects to the server given in SERVER_NAME:SERVER_PORT, and sends the Host header in HTTP_HOST -- they do not have to match. You can send requests to servers despite what DNS says. Set ``environ['webob.client.timeout'] = 10`` to set the timeout on the request (to, for example, 10 seconds). Does not add X-Forwarded-For or other standard headers If you use ``send_request_app`` then simple ``httplib`` connections will be used. """ def __init__(self, HTTPConnection=httplib.HTTPConnection, HTTPSConnection=httplib.HTTPSConnection): self.HTTPConnection = HTTPConnection self.HTTPSConnection = HTTPSConnection def __call__(self, environ, start_response): scheme = environ['wsgi.url_scheme'] if scheme == 'http': ConnClass = self.HTTPConnection elif scheme == 'https': ConnClass = self.HTTPSConnection else: raise ValueError( "Unknown scheme: %r" % scheme) if 'SERVER_NAME' not in environ: host = environ.get('HTTP_HOST') if not host: raise ValueError( "environ contains neither SERVER_NAME nor HTTP_HOST") if ':' in host: host, port = host.split(':', 1) else: if scheme == 'http': port = '80' else: port = '443' environ['SERVER_NAME'] = host environ['SERVER_PORT'] = port kw = {} if ('webob.client.timeout' in environ and self._timeout_supported(ConnClass) ): kw['timeout'] = environ['webob.client.timeout'] conn = ConnClass('%(SERVER_NAME)s:%(SERVER_PORT)s' % environ, **kw) headers = {} for key, value in environ.items(): if key.startswith('HTTP_'): key = key[5:].replace('_', '-').title() headers[key] = value path = (url_quote(environ.get('SCRIPT_NAME', '')) + url_quote(environ.get('PATH_INFO', ''))) if environ.get('QUERY_STRING'): path += '?' + environ['QUERY_STRING'] try: content_length = int(environ.get('CONTENT_LENGTH', '0')) except ValueError: content_length = 0 ## FIXME: there is no streaming of the body, and that might be useful ## in some cases if content_length: body = environ['wsgi.input'].read(content_length) else: body = '' headers['Content-Length'] = content_length if environ.get('CONTENT_TYPE'): headers['Content-Type'] = environ['CONTENT_TYPE'] if not path.startswith("/"): path = "/" + path try: conn.request(environ['REQUEST_METHOD'], path, body, headers) res = conn.getresponse() except socket.timeout: resp = exc.HTTPGatewayTimeout() return resp(environ, start_response) except (socket.error, socket.gaierror) as e: if ((isinstance(e, socket.error) and e.args[0] == -2) or (isinstance(e, socket.gaierror) and e.args[0] == 8)): # Name or service not known resp = exc.HTTPBadGateway( "Name or service not known (bad domain name: %s)" % environ['SERVER_NAME']) return resp(environ, start_response) elif e.args[0] in _e_refused: # pragma: no cover # Connection refused resp = exc.HTTPBadGateway("Connection refused") return resp(environ, start_response) raise headers_out = self.parse_headers(res.msg) status = '%s %s' % (res.status, res.reason) start_response(status, headers_out) length = res.getheader('content-length') # FIXME: This shouldn't really read in all the content at once if length is not None: body = res.read(int(length)) else: body = res.read() conn.close() return [body] # Remove these headers from response (specify lower case header # names): filtered_headers = ( 'transfer-encoding', ) MULTILINE_RE = re.compile(r'\r?\n\s*') def parse_headers(self, message): """ Turn a Message object into a list of WSGI-style headers. """ headers_out = [] if PY3: # pragma: no cover headers = message._headers else: # pragma: no cover headers = message.headers for full_header in headers: if not full_header: # pragma: no cover # Shouldn't happen, but we'll just ignore continue if full_header[0].isspace(): # pragma: no cover # Continuation line, add to the last header if not headers_out: raise ValueError( "First header starts with a space (%r)" % full_header) last_header, last_value = headers_out.pop() value = last_value + ', ' + full_header.strip() headers_out.append((last_header, value)) continue if isinstance(full_header, tuple): # pragma: no cover header, value = full_header else: # pragma: no cover try: header, value = full_header.split(':', 1) except: raise ValueError("Invalid header: %r" % (full_header,)) value = value.strip() if '\n' in value or '\r\n' in value: # pragma: no cover # Python 3 has multiline values for continuations, Python 2 # has two items in headers value = self.MULTILINE_RE.sub(', ', value) if header.lower() not in self.filtered_headers: headers_out.append((header, value)) return headers_out def _timeout_supported(self, ConnClass): if sys.version_info < (2, 7) and ConnClass in ( httplib.HTTPConnection, httplib.HTTPSConnection): # pragma: no cover return False return True send_request_app = SendRequest() _e_refused = (errno.ECONNREFUSED,) if hasattr(errno, 'ENODATA'): # pragma: no cover _e_refused += (errno.ENODATA,) WebOb-1.3.1/webob/cookies.py0000664000175000017500000006442012252633462016427 0ustar chrismchrism00000000000000import collections import base64 import binascii import hashlib import hmac import json from datetime import ( date, datetime, timedelta, ) import re import string import time from webob.compat import ( PY3, text_type, bytes_, text_, native_, string_types, ) from webob.util import strings_differ __all__ = ['Cookie', 'CookieProfile', 'SignedCookieProfile', 'SignedSerializer', 'JSONSerializer', 'make_cookie'] _marker = object() class RequestCookies(collections.MutableMapping): _cache_key = 'webob._parsed_cookies' def __init__(self, environ): self._environ = environ @property def _cache(self): env = self._environ header = env.get('HTTP_COOKIE', '') cache, cache_header = env.get(self._cache_key, ({}, None)) if cache_header == header: return cache d = lambda b: b.decode('utf8') cache = dict((d(k), d(v)) for k,v in parse_cookie(header)) env[self._cache_key] = (cache, header) return cache def _mutate_header(self, name, value): header = self._environ.get('HTTP_COOKIE') had_header = header is not None header = header or '' if PY3: # pragma: no cover header = header.encode('latin-1') bytes_name = bytes_(name, 'ascii') if value is None: replacement = None else: bytes_val = _value_quote(bytes_(value, 'utf-8')) replacement = bytes_name + b'=' + bytes_val matches = _rx_cookie.finditer(header) found = False for match in matches: start, end = match.span() match_name = match.group(1) if match_name == bytes_name: found = True if replacement is None: # remove value header = header[:start].rstrip(b' ;') + header[end:] else: # replace value header = header[:start] + replacement + header[end:] break else: if replacement is not None: if header: header += b'; ' + replacement else: header = replacement if header: self._environ['HTTP_COOKIE'] = native_(header, 'latin-1') elif had_header: self._environ['HTTP_COOKIE'] = '' return found def _valid_cookie_name(self, name): if not isinstance(name, string_types): raise TypeError(name, 'cookie name must be a string') if not isinstance(name, text_type): name = text_(name, 'utf-8') try: bytes_cookie_name = bytes_(name, 'ascii') except UnicodeEncodeError: raise TypeError('cookie name must be encodable to ascii') if not _valid_cookie_name(bytes_cookie_name): raise TypeError('cookie name must be valid according to RFC 6265') return name def __setitem__(self, name, value): name = self._valid_cookie_name(name) if not isinstance(value, string_types): raise ValueError(value, 'cookie value must be a string') if not isinstance(value, text_type): try: value = text_(value, 'utf-8') except UnicodeDecodeError: raise ValueError( value, 'cookie value must be utf-8 binary or unicode') self._mutate_header(name, value) def __getitem__(self, name): return self._cache[name] def get(self, name, default=None): return self._cache.get(name, default) def __delitem__(self, name): name = self._valid_cookie_name(name) found = self._mutate_header(name, None) if not found: raise KeyError(name) def keys(self): return self._cache.keys() def values(self): return self._cache.values() def items(self): return self._cache.items() if not PY3: def iterkeys(self): return self._cache.iterkeys() def itervalues(self): return self._cache.itervalues() def iteritems(self): return self._cache.iteritems() def __contains__(self, name): return name in self._cache def __iter__(self): return self._cache.__iter__() def __len__(self): return len(self._cache) def clear(self): self._environ['HTTP_COOKIE'] = '' def __repr__(self): return '' % (self._cache,) class Cookie(dict): def __init__(self, input=None): if input: self.load(input) def load(self, data): morsel = {} for key, val in _parse_cookie(data): if key.lower() in _c_keys: morsel[key] = val else: morsel = self.add(key, val) def add(self, key, val): if not isinstance(key, bytes): key = key.encode('ascii', 'replace') if not _valid_cookie_name(key): return {} r = Morsel(key, val) dict.__setitem__(self, key, r) return r __setitem__ = add def serialize(self, full=True): return '; '.join(m.serialize(full) for m in self.values()) def values(self): return [m for _, m in sorted(self.items())] __str__ = serialize def __repr__(self): return '<%s: [%s]>' % (self.__class__.__name__, ', '.join(map(repr, self.values()))) def _parse_cookie(data): if PY3: # pragma: no cover data = data.encode('latin-1') for key, val in _rx_cookie.findall(data): yield key, _unquote(val) def parse_cookie(data): """ Parse cookies ignoring anything except names and values """ return ((k,v) for k,v in _parse_cookie(data) if _valid_cookie_name(k)) def cookie_property(key, serialize=lambda v: v): def fset(self, v): self[key] = serialize(v) return property(lambda self: self[key], fset) def serialize_max_age(v): if isinstance(v, timedelta): v = str(v.seconds + v.days*24*60*60) elif isinstance(v, int): v = str(v) return bytes_(v) def serialize_cookie_date(v): if v is None: return None elif isinstance(v, bytes): return v elif isinstance(v, text_type): return v.encode('ascii') elif isinstance(v, int): v = timedelta(seconds=v) if isinstance(v, timedelta): v = datetime.utcnow() + v if isinstance(v, (datetime, date)): v = v.timetuple() r = time.strftime('%%s, %d-%%s-%Y %H:%M:%S GMT', v) return bytes_(r % (weekdays[v[6]], months[v[1]]), 'ascii') class Morsel(dict): __slots__ = ('name', 'value') def __init__(self, name, value): self.name = bytes_(name, encoding='ascii') self.value = bytes_(value, encoding='ascii') assert _valid_cookie_name(self.name) self.update(dict.fromkeys(_c_keys, None)) path = cookie_property(b'path') domain = cookie_property(b'domain') comment = cookie_property(b'comment') expires = cookie_property(b'expires', serialize_cookie_date) max_age = cookie_property(b'max-age', serialize_max_age) httponly = cookie_property(b'httponly', bool) secure = cookie_property(b'secure', bool) def __setitem__(self, k, v): k = bytes_(k.lower(), 'ascii') if k in _c_keys: dict.__setitem__(self, k, v) def serialize(self, full=True): result = [] add = result.append add(self.name + b'=' + _value_quote(self.value)) if full: for k in _c_valkeys: v = self[k] if v: info = _c_renames[k] name = info['name'] quoter = info['quoter'] add(name + b'=' + quoter(v)) expires = self[b'expires'] if expires: add(b'expires=' + expires) if self.secure: add(b'secure') if self.httponly: add(b'HttpOnly') return native_(b'; '.join(result), 'ascii') __str__ = serialize def __repr__(self): return '<%s: %s=%r>' % (self.__class__.__name__, native_(self.name), native_(self.value) ) # # parsing # _re_quoted = r'"(?:\\"|.)*?"' # any doublequoted string _legal_special_chars = "~!@#$%^&*()_+=-`.?|:/(){}<>'" _re_legal_char = r"[\w\d%s]" % re.escape(_legal_special_chars) _re_expires_val = r"\w{3},\s[\w\d-]{9,11}\s[\d:]{8}\sGMT" _re_cookie_str_key = r"(%s+?)" % _re_legal_char _re_cookie_str_equal = r"\s*=\s*" _re_unquoted_val = r"(?:%s|\\(?:[0-3][0-7][0-7]|.))*" % _re_legal_char _re_cookie_str_val = r"(%s|%s|%s)" % (_re_quoted, _re_expires_val, _re_unquoted_val) _re_cookie_str = _re_cookie_str_key + _re_cookie_str_equal + _re_cookie_str_val _rx_cookie = re.compile(bytes_(_re_cookie_str, 'ascii')) _rx_unquote = re.compile(bytes_(r'\\([0-3][0-7][0-7]|.)', 'ascii')) _bchr = (lambda i: bytes([i])) if PY3 else chr _ch_unquote_map = dict((bytes_('%03o' % i), _bchr(i)) for i in range(256) ) _ch_unquote_map.update((v, v) for v in list(_ch_unquote_map.values())) _b_dollar_sign = ord('$') if PY3 else '$' _b_quote_mark = ord('"') if PY3 else '"' def _unquote(v): #assert isinstance(v, bytes) if v and v[0] == v[-1] == _b_quote_mark: v = v[1:-1] return _rx_unquote.sub(_ch_unquote, v) def _ch_unquote(m): return _ch_unquote_map[m.group(1)] # # serializing # # these chars can be in cookie value w/o causing it to be quoted # see http://tools.ietf.org/html/rfc6265#section-4.1.1 # and https://github.com/Pylons/webob/pull/104#issuecomment-28044314 # allowed in cookie values without quoting: # (0x21), "#$%&'()*+" (0x25-0x2B), "-./0123456789:" (0x2D-0x3A), # "<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[" (0x3C-0x5B), # "]^_`abcdefghijklmnopqrstuvwxyz{|}~" (0x5D-0x7E) _no_escape_special_chars = "!#$%&'()*+-./<=>?@[]^_`{|}~" _no_escape_chars = (string.ascii_letters + string.digits + _no_escape_special_chars) _no_escape_bytes = bytes_(_no_escape_chars) # these chars should not be quoted themselves but if they are present they # should cause the cookie value to be surrounded by quotes (voodoo inherited # by old webob code without any comments) _escape_noop_chars = _no_escape_chars + ': ' # this is a map used to escape the values _escape_map = dict((chr(i), '\\%03o' % i) for i in range(256)) _escape_map.update(zip(_escape_noop_chars, _escape_noop_chars)) if PY3: # pragma: no cover # convert to {int -> bytes} _escape_map = dict( (ord(k), bytes_(v, 'ascii')) for k, v in _escape_map.items() ) _escape_char = _escape_map.__getitem__ weekdays = ('Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun') months = (None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec') _notrans_binary = b' '*256 # these are the characters accepted in cookie *names* _valid_token_chars = string.ascii_letters + string.digits + "!#$%&'*+,-.^_`|~" _valid_token_bytes = bytes_(_valid_token_chars) def _value_needs_quoting(v): return v.translate(_notrans_binary, _no_escape_bytes) def _value_quote(v): #assert isinstance(v, bytes) if _value_needs_quoting(v): return b'"' + b''.join(map(_escape_char, v)) + b'"' return v def _valid_cookie_name(key): return isinstance(key, bytes) and not ( key.translate(_notrans_binary, _valid_token_bytes) or key[0] == _b_dollar_sign or key.lower() in _c_keys ) def _path_quote(v): return b''.join(map(_escape_char, v)) _domain_quote = _path_quote _max_age_quote = _path_quote _c_renames = { b"path" : {'name':b"Path", 'quoter':_path_quote}, b"comment" : {'name':b"Comment", 'quoter':_value_quote}, b"domain" : {'name':b"Domain", 'quoter':_domain_quote}, b"max-age" : {'name':b"Max-Age", 'quoter':_max_age_quote}, } _c_valkeys = sorted(_c_renames) _c_keys = set(_c_renames) _c_keys.update([b'expires', b'secure', b'httponly']) def make_cookie(name, value, max_age=None, path='/', domain=None, secure=False, httponly=False, comment=None): """ Generate a cookie value. If ``value`` is None, generate a cookie value with an expiration date in the past""" # We are deleting the cookie, override max_age and expires if value is None: value = b'' # Note that the max-age value of zero is technically contraspec; # RFC6265 says that max-age cannot be zero. However, all browsers # appear to support this to mean "delete immediately". # http://www.timwilson.id.au/news-three-critical-problems-with-rfc6265.html max_age = 0 expires = 'Wed, 31-Dec-97 23:59:59 GMT' # Convert max_age to seconds elif isinstance(max_age, timedelta): max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds expires = max_age else: expires = max_age morsel = Morsel(name, value) if domain is not None: morsel.domain = bytes_(domain) if path is not None: morsel.path = bytes_(path) if httponly: morsel.httponly = True if secure: morsel.secure = True if max_age is not None: morsel.max_age = max_age if expires is not None: morsel.expires = expires if comment is not None: morsel.comment = bytes_(comment) return morsel.serialize() class JSONSerializer(object): """ A serializer which uses `json.dumps`` and ``json.loads``""" def dumps(self, appstruct): return bytes_(json.dumps(appstruct), encoding='utf-8') def loads(self, bstruct): # NB: json.loads raises ValueError if no json object can be decoded # so we don't have to do it explicitly here. return json.loads(text_(bstruct, encoding='utf-8')) class SignedSerializer(object): """ A helper to cryptographically sign arbitrary content using HMAC. The serializer accepts arbitrary functions for performing the actual serialization and deserialization. ``secret`` A string which is used to sign the cookie. The secret should be at least as long as the block size of the selected hash algorithm. For ``sha512`` this would mean a 128 bit (64 character) secret. ``salt`` A namespace to avoid collisions between different uses of a shared secret. ``hashalg`` The HMAC digest algorithm to use for signing. The algorithm must be supported by the :mod:`hashlib` library. Default: ``'sha512'``. ``serializer`` An object with two methods: `loads`` and ``dumps``. The ``loads`` method should accept bytes and return a Python object. The ``dumps`` method should accept a Python object and return bytes. A ``ValueError`` should be raised for malformed inputs. Default: ``None`, which will use a derivation of :func:`json.dumps` and ``json.loads``. """ def __init__(self, secret, salt, hashalg='sha512', serializer=None, ): self.salt = salt self.secret = secret self.hashalg = hashalg self.salted_secret = bytes_(salt or '') + bytes_(secret) self.digestmod = lambda string=b'': hashlib.new(self.hashalg, string) self.digest_size = self.digestmod().digest_size if serializer is None: serializer = JSONSerializer() self.serializer = serializer def dumps(self, appstruct): """ Given an ``appstruct``, serialize and sign the data. Returns a bytestring. """ cstruct = self.serializer.dumps(appstruct) # will be bytes sig = hmac.new(self.salted_secret, cstruct, self.digestmod).digest() return base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=') def loads(self, bstruct): """ Given a ``bstruct`` (a bytestring), verify the signature and then deserialize and return the deserialized value. A ``ValueError`` will be raised if the signature fails to validate. """ try: b64padding = b'=' * (-len(bstruct) % 4) fstruct = base64.urlsafe_b64decode(bytes_(bstruct) + b64padding) except (binascii.Error, TypeError) as e: raise ValueError('Badly formed base64 data: %s' % e) cstruct = fstruct[self.digest_size:] expected_sig = fstruct[:self.digest_size] sig = hmac.new( self.salted_secret, bytes_(cstruct), self.digestmod).digest() if strings_differ(sig, expected_sig): raise ValueError('Invalid signature') return self.serializer.loads(cstruct) _default = object() class CookieProfile(object): """ A helper class that helps bring some sanity to the insanity that is cookie handling. The helper is capable of generating multiple cookies if necessary to support subdomains and parent domains. ``cookie_name`` The name of the cookie used for sessioning. Default: ``'session'``. ``max_age`` The maximum age of the cookie used for sessioning (in seconds). Default: ``None`` (browser scope). ``secure`` The 'secure' flag of the session cookie. Default: ``False``. ``httponly`` Hide the cookie from Javascript by setting the 'HttpOnly' flag of the session cookie. Default: ``False``. ``path`` The path used for the session cookie. Default: ``'/'``. ``domains`` The domain(s) used for the session cookie. Default: ``None`` (no domain). Can be passed an iterable containing multiple domains, this will set multiple cookies one for each domain. ``serializer`` An object with two methods: `loads`` and ``dumps``. The ``loads`` method should accept a bytestring and return a Python object. The ``dumps`` method should accept a Python object and return bytes. A ``ValueError`` should be raised for malformed inputs. Default: ``None`, which will use a derivation of :func:`json.dumps` and ``json.loads``. """ def __init__(self, cookie_name, secure=False, max_age=None, httponly=None, path='/', domains=None, serializer=None ): self.cookie_name = cookie_name self.secure = secure self.max_age = max_age self.httponly = httponly self.path = path self.domains = domains if serializer is None: serializer = JSONSerializer() self.serializer = serializer self.request = None def __call__(self, request): """ Bind a request to a copy of this instance and return it""" return self.bind(request) def bind(self, request): """ Bind a request to a copy of this instance and return it""" selfish = CookieProfile( self.cookie_name, self.secure, self.max_age, self.httponly, self.path, self.domains, self.serializer, ) selfish.request = request return selfish def get_value(self): """ Looks for a cookie by name in the currently bound request, and returns its value. If the cookie profile is not bound to a request, this method will raise a :exc:`ValueError`. Looks for the cookie in the cookies jar, and if it can find it it will attempt to deserialize it. Returns ``None`` if there is no cookie or if the value in the cookie cannot be successfully deserialized. """ if not self.request: raise ValueError('No request bound to cookie profile') cookie = self.request.cookies.get(self.cookie_name) if cookie is not None: try: return self.serializer.loads(bytes_(cookie)) except ValueError: return None def set_cookies(self, response, value, domains=_default, max_age=_default, path=_default, secure=_default, httponly=_default): """ Set the cookies on a response.""" cookies = self.get_headers( value, domains=domains, max_age=max_age, path=path, secure=secure, httponly=httponly ) response.headerlist.extend(cookies) return response def get_headers(self, value, domains=_default, max_age=_default, path=_default, secure=_default, httponly=_default): """ Retrieve raw headers for setting cookies. Returns a list of headers that should be set for the cookies to be correctly tracked. """ if value is None: max_age = 0 bstruct = None else: bstruct = self.serializer.dumps(value) return self._get_cookies( bstruct, domains=domains, max_age=max_age, path=path, secure=secure, httponly=httponly ) def _get_cookies(self, value, domains, max_age, path, secure, httponly): """Internal function This returns a list of cookies that are valid HTTP Headers. :environ: The request environment :value: The value to store in the cookie :domains: The domains, overrides any set in the CookieProfile :max_age: The max_age, overrides any set in the CookieProfile :path: The path, overrides any set in the CookieProfile :secure: Set this cookie to secure, overrides any set in CookieProfile :httponly: Set this cookie to HttpOnly, overrides any set in CookieProfile """ # If the user doesn't provide values, grab the defaults if domains is _default: domains = self.domains if max_age is _default: max_age = self.max_age if path is _default: path = self.path if secure is _default: secure = self.secure if httponly is _default: httponly = self.httponly # Length selected based upon http://browsercookielimits.x64.me if value is not None and len(value) > 4093: raise ValueError( 'Cookie value is too long to store (%s bytes)' % len(value) ) cookies = [] if not domains: cookievalue = make_cookie( self.cookie_name, value, path=path, max_age=max_age, httponly=httponly, secure=secure ) cookies.append(('Set-Cookie', cookievalue)) else: for domain in domains: cookievalue = make_cookie( self.cookie_name, value, path=path, domain=domain, max_age=max_age, httponly=httponly, secure=secure, ) cookies.append(('Set-Cookie', cookievalue)) return cookies class SignedCookieProfile(CookieProfile): """ A helper for generating cookies that are signed to prevent tampering. By default this will create a single cookie, given a value it will serialize it, then use HMAC to cryptographically sign the data. Finally the result is base64-encoded for transport. This way a remote user can not tamper with the value without uncovering the secret/salt used. ``secret`` A string which is used to sign the cookie. The secret should be at least as long as the block size of the selected hash algorithm. For ``sha512`` this would mean a 128 bit (64 character) secret. ``salt`` A namespace to avoid collisions between different uses of a shared secret. ``hashalg`` The HMAC digest algorithm to use for signing. The algorithm must be supported by the :mod:`hashlib` library. Default: ``'sha512'``. ``cookie_name`` The name of the cookie used for sessioning. Default: ``'session'``. ``max_age`` The maximum age of the cookie used for sessioning (in seconds). Default: ``None`` (browser scope). ``secure`` The 'secure' flag of the session cookie. Default: ``False``. ``httponly`` Hide the cookie from Javascript by setting the 'HttpOnly' flag of the session cookie. Default: ``False``. ``path`` The path used for the session cookie. Default: ``'/'``. ``domains`` The domain(s) used for the session cookie. Default: ``None`` (no domain). Can be passed an iterable containing multiple domains, this will set multiple cookies one for each domain. ``serializer`` An object with two methods: `loads`` and ``dumps``. The ``loads`` method should accept bytes and return a Python object. The ``dumps`` method should accept a Python object and return bytes. A ``ValueError`` should be raised for malformed inputs. Default: ``None`, which will use a derivation of :func:`json.dumps` and ``json.loads``. """ def __init__(self, secret, salt, cookie_name, secure=False, max_age=None, httponly=False, path="/", domains=None, hashalg='sha512', serializer=None, ): self.secret = secret self.salt = salt self.hashalg = hashalg self.original_serializer = serializer signed_serializer = SignedSerializer( secret, salt, hashalg, serializer=self.original_serializer, ) CookieProfile.__init__( self, cookie_name, secure=secure, max_age=max_age, httponly=httponly, path=path, domains=domains, serializer=signed_serializer, ) def bind(self, request): """ Bind a request to a copy of this instance and return it""" selfish = SignedCookieProfile( self.secret, self.salt, self.cookie_name, self.secure, self.max_age, self.httponly, self.path, self.domains, self.hashalg, self.original_serializer, ) selfish.request = request return selfish WebOb-1.3.1/webob/acceptparse.py0000664000175000017500000002417712047064524017271 0ustar chrismchrism00000000000000""" Parses a variety of ``Accept-*`` headers. These headers generally take the form of:: value1; q=0.5, value2; q=0 Where the ``q`` parameter is optional. In theory other parameters exists, but this ignores them. """ import re from webob.headers import _trans_name as header_to_key from webob.util import ( header_docstring, warn_deprecation, ) part_re = re.compile( r',\s*([^\s;,\n]+)(?:[^,]*?;\s*q=([0-9.]*))?') def _warn_first_match(): # TODO: remove .first_match in version 1.3 warn_deprecation("Use best_match instead", '1.2', 3) class Accept(object): """ Represents a generic ``Accept-*`` style header. This object should not be modified. To add items you can use ``accept_obj + 'accept_thing'`` to get a new object """ def __init__(self, header_value): self.header_value = header_value self._parsed = list(self.parse(header_value)) self._parsed_nonzero = [(m,q) for (m,q) in self._parsed if q] @staticmethod def parse(value): """ Parse ``Accept-*`` style header. Return iterator of ``(value, quality)`` pairs. ``quality`` defaults to 1. """ for match in part_re.finditer(','+value): name = match.group(1) if name == 'q': continue quality = match.group(2) or '' if quality: try: quality = max(min(float(quality), 1), 0) yield (name, quality) continue except ValueError: pass yield (name, 1) def __repr__(self): return '<%s(%r)>' % (self.__class__.__name__, str(self)) def __iter__(self): for m,q in sorted( self._parsed_nonzero, key=lambda i: i[1], reverse=True ): yield m def __str__(self): result = [] for mask, quality in self._parsed: if quality != 1: mask = '%s;q=%0.*f' % ( mask, min(len(str(quality).split('.')[1]), 3), quality) result.append(mask) return ', '.join(result) def __add__(self, other, reversed=False): if isinstance(other, Accept): other = other.header_value if hasattr(other, 'items'): other = sorted(other.items(), key=lambda item: -item[1]) if isinstance(other, (list, tuple)): result = [] for item in other: if isinstance(item, (list, tuple)): name, quality = item result.append('%s; q=%s' % (name, quality)) else: result.append(item) other = ', '.join(result) other = str(other) my_value = self.header_value if reversed: other, my_value = my_value, other if not other: new_value = my_value elif not my_value: new_value = other else: new_value = my_value + ', ' + other return self.__class__(new_value) def __radd__(self, other): return self.__add__(other, True) def __contains__(self, offer): """ Returns true if the given object is listed in the accepted types. """ for mask, quality in self._parsed_nonzero: if self._match(mask, offer): return True def quality(self, offer, modifier=1): """ Return the quality of the given offer. Returns None if there is no match (not 0). """ bestq = 0 for mask, q in self._parsed: if self._match(mask, offer): bestq = max(bestq, q * modifier) return bestq or None def first_match(self, offers): """ DEPRECATED Returns the first allowed offered type. Ignores quality. Returns the first offered type if nothing else matches; or if you include None at the end of the match list then that will be returned. """ _warn_first_match() def best_match(self, offers, default_match=None): """ Returns the best match in the sequence of offered types. The sequence can be a simple sequence, or you can have ``(match, server_quality)`` items in the sequence. If you have these tuples then the client quality is multiplied by the server_quality to get a total. If two matches have equal weight, then the one that shows up first in the `offers` list will be returned. But among matches with the same quality the match to a more specific requested type will be chosen. For example a match to text/* trumps */*. default_match (default None) is returned if there is no intersection. """ best_quality = -1 best_offer = default_match matched_by = '*/*' for offer in offers: if isinstance(offer, (tuple, list)): offer, server_quality = offer else: server_quality = 1 for mask, quality in self._parsed_nonzero: possible_quality = server_quality * quality if possible_quality < best_quality: continue elif possible_quality == best_quality: # 'text/plain' overrides 'message/*' overrides '*/*' # (if all match w/ the same q=) if matched_by.count('*') <= mask.count('*'): continue if self._match(mask, offer): best_quality = possible_quality best_offer = offer matched_by = mask return best_offer def _match(self, mask, offer): _check_offer(offer) return mask == '*' or offer.lower() == mask.lower() class NilAccept(object): MasterClass = Accept def __repr__(self): return '<%s: %s>' % (self.__class__.__name__, self.MasterClass) def __str__(self): return '' def __nonzero__(self): return False __bool__ = __nonzero__ # python 3 def __iter__(self): return iter(()) def __add__(self, item): if isinstance(item, self.MasterClass): return item else: return self.MasterClass('') + item def __radd__(self, item): if isinstance(item, self.MasterClass): return item else: return item + self.MasterClass('') def __contains__(self, item): _check_offer(item) return True def quality(self, offer, default_quality=1): return 0 def first_match(self, offers): # pragma: no cover _warn_first_match() def best_match(self, offers, default_match=None): best_quality = -1 best_offer = default_match for offer in offers: _check_offer(offer) if isinstance(offer, (list, tuple)): offer, quality = offer else: quality = 1 if quality > best_quality: best_offer = offer best_quality = quality return best_offer class NoAccept(NilAccept): def __contains__(self, item): return False class AcceptCharset(Accept): @staticmethod def parse(value): latin1_found = False for m, q in Accept.parse(value): _m = m.lower() if _m == '*' or _m == 'iso-8859-1': latin1_found = True yield _m, q if not latin1_found: yield ('iso-8859-1', 1) class AcceptLanguage(Accept): def _match(self, mask, item): item = item.replace('_', '-').lower() mask = mask.lower() return (mask == '*' or item == mask or item.split('-')[0] == mask or item == mask.split('-')[0] ) class MIMEAccept(Accept): """ Represents the ``Accept`` header, which is a list of mimetypes. This class knows about mime wildcards, like ``image/*`` """ @staticmethod def parse(value): for mask, q in Accept.parse(value): try: mask_major, mask_minor = map(lambda x: x.lower(), mask.split('/')) except ValueError: continue if mask_major == '*' and mask_minor != '*': continue if mask_major != "*" and "*" in mask_major: continue if mask_minor != "*" and "*" in mask_minor: continue yield ("%s/%s" % (mask_major, mask_minor), q) def accept_html(self): """ Returns true if any HTML-like type is accepted """ return ('text/html' in self or 'application/xhtml+xml' in self or 'application/xml' in self or 'text/xml' in self) accepts_html = property(accept_html) # note the plural def _match(self, mask, offer): """ Check if the offer is covered by the mask """ _check_offer(offer) if '*' not in mask: return offer.lower() == mask.lower() elif mask == '*/*': return True else: assert mask.endswith('/*') mask_major = mask[:-2].lower() offer_major = offer.split('/', 1)[0].lower() return offer_major == mask_major class MIMENilAccept(NilAccept): MasterClass = MIMEAccept def _check_offer(offer): if '*' in offer: raise ValueError("The application should offer specific types, got %r" % offer) def accept_property(header, rfc_section, AcceptClass=Accept, NilClass=NilAccept ): key = header_to_key(header) doc = header_docstring(header, rfc_section) #doc += " Converts it as a %s." % convert_name def fget(req): value = req.environ.get(key) if not value: return NilClass() return AcceptClass(value) def fset(req, val): if val: if isinstance(val, (list, tuple, dict)): val = AcceptClass('') + val val = str(val) req.environ[key] = val or None def fdel(req): del req.environ[key] return property(fget, fset, fdel, doc) WebOb-1.3.1/webob/__init__.py0000664000175000017500000000051412044266064016523 0ustar chrismchrism00000000000000from webob.datetime_utils import * from webob.request import * from webob.response import * from webob.util import html_escape __all__ = [ 'Request', 'LegacyRequest', 'Response', 'UTC', 'day', 'week', 'hour', 'minute', 'second', 'month', 'year', 'html_escape' ] BaseRequest.ResponseClass = Response __version__ = '1.2.3' WebOb-1.3.1/webob/cachecontrol.py0000664000175000017500000001471512123261311017424 0ustar chrismchrism00000000000000""" Represents the Cache-Control header """ import re class UpdateDict(dict): """ Dict that has a callback on all updates """ # these are declared as class attributes so that # we don't need to override constructor just to # set some defaults updated = None updated_args = None def _updated(self): """ Assign to new_dict.updated to track updates """ updated = self.updated if updated is not None: args = self.updated_args if args is None: args = (self,) updated(*args) def __setitem__(self, key, item): dict.__setitem__(self, key, item) self._updated() def __delitem__(self, key): dict.__delitem__(self, key) self._updated() def clear(self): dict.clear(self) self._updated() def update(self, *args, **kw): dict.update(self, *args, **kw) self._updated() def setdefault(self, key, value=None): val = dict.setdefault(self, key, value) if val is value: self._updated() return val def pop(self, *args): v = dict.pop(self, *args) self._updated() return v def popitem(self): v = dict.popitem(self) self._updated() return v token_re = re.compile( r'([a-zA-Z][a-zA-Z_-]*)\s*(?:=(?:"([^"]*)"|([^ \t",;]*)))?') need_quote_re = re.compile(r'[^a-zA-Z0-9._-]') class exists_property(object): """ Represents a property that either is listed in the Cache-Control header, or is not listed (has no value) """ def __init__(self, prop, type=None): self.prop = prop self.type = type def __get__(self, obj, type=None): if obj is None: return self return self.prop in obj.properties def __set__(self, obj, value): if (self.type is not None and self.type != obj.type): raise AttributeError( "The property %s only applies to %s Cache-Control" % ( self.prop, self.type)) if value: obj.properties[self.prop] = None else: if self.prop in obj.properties: del obj.properties[self.prop] def __delete__(self, obj): self.__set__(obj, False) class value_property(object): """ Represents a property that has a value in the Cache-Control header. When no value is actually given, the value of self.none is returned. """ def __init__(self, prop, default=None, none=None, type=None): self.prop = prop self.default = default self.none = none self.type = type def __get__(self, obj, type=None): if obj is None: return self if self.prop in obj.properties: value = obj.properties[self.prop] if value is None: return self.none else: return value else: return self.default def __set__(self, obj, value): if (self.type is not None and self.type != obj.type): raise AttributeError( "The property %s only applies to %s Cache-Control" % ( self.prop, self.type)) if value == self.default: if self.prop in obj.properties: del obj.properties[self.prop] elif value is True: obj.properties[self.prop] = None # Empty value, but present else: obj.properties[self.prop] = value def __delete__(self, obj): if self.prop in obj.properties: del obj.properties[self.prop] class CacheControl(object): """ Represents the Cache-Control header. By giving a type of ``'request'`` or ``'response'`` you can control what attributes are allowed (some Cache-Control values only apply to requests or responses). """ update_dict = UpdateDict def __init__(self, properties, type): self.properties = properties self.type = type @classmethod def parse(cls, header, updates_to=None, type=None): """ Parse the header, returning a CacheControl object. The object is bound to the request or response object ``updates_to``, if that is given. """ if updates_to: props = cls.update_dict() props.updated = updates_to else: props = {} for match in token_re.finditer(header): name = match.group(1) value = match.group(2) or match.group(3) or None if value: try: value = int(value) except ValueError: pass props[name] = value obj = cls(props, type=type) if updates_to: props.updated_args = (obj,) return obj def __repr__(self): return '' % str(self) # Request values: # no-cache shared (below) # no-store shared (below) # max-age shared (below) max_stale = value_property('max-stale', none='*', type='request') min_fresh = value_property('min-fresh', type='request') # no-transform shared (below) only_if_cached = exists_property('only-if-cached', type='request') # Response values: public = exists_property('public', type='response') private = value_property('private', none='*', type='response') no_cache = value_property('no-cache', none='*') no_store = exists_property('no-store') no_transform = exists_property('no-transform') must_revalidate = exists_property('must-revalidate', type='response') proxy_revalidate = exists_property('proxy-revalidate', type='response') max_age = value_property('max-age', none=-1) s_maxage = value_property('s-maxage', type='response') s_max_age = s_maxage def __str__(self): return serialize_cache_control(self.properties) def copy(self): """ Returns a copy of this object. """ return self.__class__(self.properties.copy(), type=self.type) def serialize_cache_control(properties): if isinstance(properties, CacheControl): properties = properties.properties parts = [] for name, value in sorted(properties.items()): if value is None: parts.append(name) continue value = str(value) if need_quote_re.search(value): value = '"%s"' % value parts.append('%s=%s' % (name, value)) return ', '.join(parts) WebOb-1.3.1/webob/headers.py0000644000175000017500000001022711711122014016361 0ustar chrismchrism00000000000000from collections import MutableMapping from webob.compat import ( iteritems_, string_types, ) from webob.multidict import MultiDict __all__ = ['ResponseHeaders', 'EnvironHeaders'] class ResponseHeaders(MultiDict): """ Dictionary view on the response headerlist. Keys are normalized for case and whitespace. """ def __getitem__(self, key): key = key.lower() for k, v in reversed(self._items): if k.lower() == key: return v raise KeyError(key) def getall(self, key): key = key.lower() result = [] for k, v in self._items: if k.lower() == key: result.append(v) return result def mixed(self): r = self.dict_of_lists() for key, val in iteritems_(r): if len(val) == 1: r[key] = val[0] return r def dict_of_lists(self): r = {} for key, val in iteritems_(self): r.setdefault(key.lower(), []).append(val) return r def __setitem__(self, key, value): norm_key = key.lower() items = self._items for i in range(len(items)-1, -1, -1): if items[i][0].lower() == norm_key: del items[i] self._items.append((key, value)) def __delitem__(self, key): key = key.lower() items = self._items found = False for i in range(len(items)-1, -1, -1): if items[i][0].lower() == key: del items[i] found = True if not found: raise KeyError(key) def __contains__(self, key): key = key.lower() for k, v in self._items: if k.lower() == key: return True return False has_key = __contains__ def setdefault(self, key, default=None): c_key = key.lower() for k, v in self._items: if k.lower() == c_key: return v self._items.append((key, default)) return default def pop(self, key, *args): if len(args) > 1: raise TypeError("pop expected at most 2 arguments, got %s" % repr(1 + len(args))) key = key.lower() for i in range(len(self._items)): if self._items[i][0].lower() == key: v = self._items[i][1] del self._items[i] return v if args: return args[0] else: raise KeyError(key) key2header = { 'CONTENT_TYPE': 'Content-Type', 'CONTENT_LENGTH': 'Content-Length', 'HTTP_CONTENT_TYPE': 'Content_Type', 'HTTP_CONTENT_LENGTH': 'Content_Length', } header2key = dict([(v.upper(),k) for (k,v) in key2header.items()]) def _trans_key(key): if not isinstance(key, string_types): return None elif key in key2header: return key2header[key] elif key.startswith('HTTP_'): return key[5:].replace('_', '-').title() else: return None def _trans_name(name): name = name.upper() if name in header2key: return header2key[name] return 'HTTP_'+name.replace('-', '_') class EnvironHeaders(MutableMapping): """An object that represents the headers as present in a WSGI environment. This object is a wrapper (with no internal state) for a WSGI request object, representing the CGI-style HTTP_* keys as a dictionary. Because a CGI environment can only hold one value for each key, this dictionary is single-valued (unlike outgoing headers). """ def __init__(self, environ): self.environ = environ def __getitem__(self, hname): return self.environ[_trans_name(hname)] def __setitem__(self, hname, value): self.environ[_trans_name(hname)] = value def __delitem__(self, hname): del self.environ[_trans_name(hname)] def keys(self): return filter(None, map(_trans_key, self.environ)) def __contains__(self, hname): return _trans_name(hname) in self.environ def __len__(self): return len(list(self.keys())) def __iter__(self): for k in self.keys(): yield k WebOb-1.3.1/webob/byterange.py0000664000175000017500000001120212023145137016732 0ustar chrismchrism00000000000000import re __all__ = ['Range', 'ContentRange'] _rx_range = re.compile('bytes *= *(\d*) *- *(\d*)', flags=re.I) _rx_content_range = re.compile(r'bytes (?:(\d+)-(\d+)|[*])/(?:(\d+)|[*])') class Range(object): """ Represents the Range header. """ def __init__(self, start, end): assert end is None or end >= 0, "Bad range end: %r" % end self.start = start self.end = end # non-inclusive def range_for_length(self, length): """ *If* there is only one range, and *if* it is satisfiable by the given length, then return a (start, end) non-inclusive range of bytes to serve. Otherwise return None """ if length is None: return None start, end = self.start, self.end if end is None: end = length if start < 0: start += length if _is_content_range_valid(start, end, length): stop = min(end, length) return (start, stop) else: return None def content_range(self, length): """ Works like range_for_length; returns None or a ContentRange object You can use it like:: response.content_range = req.range.content_range(response.content_length) Though it's still up to you to actually serve that content range! """ range = self.range_for_length(length) if range is None: return None return ContentRange(range[0], range[1], length) def __str__(self): s,e = self.start, self.end if e is None: r = 'bytes=%s' % s if s >= 0: r += '-' return r return 'bytes=%s-%s' % (s, e-1) def __repr__(self): return '%s(%r, %r)' % ( self.__class__.__name__, self.start, self.end) def __iter__(self): return iter((self.start, self.end)) @classmethod def parse(cls, header): """ Parse the header; may return None if header is invalid """ m = _rx_range.match(header or '') if not m: return None start, end = m.groups() if not start: return cls(-int(end), None) start = int(start) if not end: return cls(start, None) end = int(end) + 1 # return val is non-inclusive if start >= end: return None return cls(start, end) class ContentRange(object): """ Represents the Content-Range header This header is ``start-stop/length``, where start-stop and length can be ``*`` (represented as None in the attributes). """ def __init__(self, start, stop, length): if not _is_content_range_valid(start, stop, length): raise ValueError( "Bad start:stop/length: %r-%r/%r" % (start, stop, length)) self.start = start self.stop = stop # this is python-style range end (non-inclusive) self.length = length def __repr__(self): return '<%s %s>' % (self.__class__.__name__, self) def __str__(self): if self.length is None: length = '*' else: length = self.length if self.start is None: assert self.stop is None return 'bytes */%s' % length stop = self.stop - 1 # from non-inclusive to HTTP-style return 'bytes %s-%s/%s' % (self.start, stop, length) def __iter__(self): """ Mostly so you can unpack this, like: start, stop, length = res.content_range """ return iter([self.start, self.stop, self.length]) @classmethod def parse(cls, value): """ Parse the header. May return None if it cannot parse. """ m = _rx_content_range.match(value or '') if not m: return None s, e, l = m.groups() if s: s = int(s) e = int(e) + 1 l = l and int(l) if not _is_content_range_valid(s, e, l, response=True): return None return cls(s, e, l) def _is_content_range_valid(start, stop, length, response=False): if (start is None) != (stop is None): return False elif start is None: return length is None or length >= 0 elif length is None: return 0 <= start < stop elif start >= stop: return False elif response and stop > length: # "content-range: bytes 0-50/10" is invalid for a response # "range: bytes 0-50" is valid for a request to a 10-bytes entity return False else: return 0 <= start < length WebOb-1.3.1/webob/etag.py0000644000175000017500000001066311712015463015704 0ustar chrismchrism00000000000000""" Does parsing of ETag-related headers: If-None-Matches, If-Matches Also If-Range parsing """ from webob.datetime_utils import ( parse_date, serialize_date, ) from webob.descriptors import _rx_etag from webob.util import ( header_docstring, warn_deprecation, ) __all__ = ['AnyETag', 'NoETag', 'ETagMatcher', 'IfRange', 'etag_property'] def etag_property(key, default, rfc_section, strong=True): doc = header_docstring(key, rfc_section) doc += " Converts it as a Etag." def fget(req): value = req.environ.get(key) if not value: return default else: return ETagMatcher.parse(value, strong=strong) def fset(req, val): if val is None: req.environ[key] = None else: req.environ[key] = str(val) def fdel(req): del req.environ[key] return property(fget, fset, fdel, doc=doc) def _warn_weak_match_deprecated(): warn_deprecation("weak_match is deprecated", '1.2', 3) def _warn_if_range_match_deprecated(*args, **kw): # pragma: no cover raise DeprecationWarning("IfRange.match[_response] API is deprecated") class _AnyETag(object): """ Represents an ETag of *, or a missing ETag when matching is 'safe' """ def __repr__(self): return '' def __nonzero__(self): return False __bool__ = __nonzero__ # python 3 def __contains__(self, other): return True def weak_match(self, other): _warn_weak_match_deprecated() def __str__(self): return '*' AnyETag = _AnyETag() class _NoETag(object): """ Represents a missing ETag when matching is unsafe """ def __repr__(self): return '' def __nonzero__(self): return False __bool__ = __nonzero__ # python 3 def __contains__(self, other): return False def weak_match(self, other): # pragma: no cover _warn_weak_match_deprecated() def __str__(self): return '' NoETag = _NoETag() # TODO: convert into a simple tuple class ETagMatcher(object): def __init__(self, etags): self.etags = etags def __contains__(self, other): return other in self.etags def weak_match(self, other): # pragma: no cover _warn_weak_match_deprecated() def __repr__(self): return '' % (' or '.join(self.etags)) @classmethod def parse(cls, value, strong=True): """ Parse this from a header value """ if value == '*': return AnyETag if not value: return cls([]) matches = _rx_etag.findall(value) if not matches: return cls([value]) elif strong: return cls([t for w,t in matches if not w]) else: return cls([t for w,t in matches]) def __str__(self): return ', '.join(map('"%s"'.__mod__, self.etags)) class IfRange(object): def __init__(self, etag): self.etag = etag @classmethod def parse(cls, value): """ Parse this from a header value. """ if not value: return cls(AnyETag) elif value.endswith(' GMT'): # Must be a date return IfRangeDate(parse_date(value)) else: return cls(ETagMatcher.parse(value)) def __contains__(self, resp): """ Return True if the If-Range header matches the given etag or last_modified """ return resp.etag_strong in self.etag def __nonzero__(self): return bool(self.etag) def __repr__(self): return '%s(%r)' % ( self.__class__.__name__, self.etag ) def __str__(self): return str(self.etag) if self.etag else '' match = match_response = _warn_if_range_match_deprecated __bool__ = __nonzero__ # python 3 class IfRangeDate(object): def __init__(self, date): self.date = date def __contains__(self, resp): last_modified = resp.last_modified #if isinstance(last_modified, str): # last_modified = parse_date(last_modified) return last_modified and (last_modified <= self.date) def __repr__(self): return '%s(%r)' % ( self.__class__.__name__, self.date #serialize_date(self.date) ) def __str__(self): return serialize_date(self.date) match = match_response = _warn_if_range_match_deprecated WebOb-1.3.1/webob/compat.py0000664000175000017500000000665512250507606016262 0ustar chrismchrism00000000000000# code stolen from "six" import sys import types # True if we are running on Python 3. PY3 = sys.version_info[0] == 3 if PY3: # pragma: no cover string_types = str, integer_types = int, class_types = type, text_type = str long = int else: string_types = basestring, integer_types = (int, long) class_types = (type, types.ClassType) text_type = unicode long = long # TODO check if errors is ever used def text_(s, encoding='latin-1', errors='strict'): if isinstance(s, bytes): return s.decode(encoding, errors) return s def bytes_(s, encoding='latin-1', errors='strict'): if isinstance(s, text_type): return s.encode(encoding, errors) return s if PY3: # pragma: no cover def native_(s, encoding='latin-1', errors='strict'): if isinstance(s, text_type): return s return str(s, encoding, errors) else: def native_(s, encoding='latin-1', errors='strict'): if isinstance(s, text_type): return s.encode(encoding, errors) return str(s) try: from queue import Queue, Empty except ImportError: from Queue import Queue, Empty if PY3: # pragma: no cover from urllib import parse urlparse = parse from urllib.parse import quote as url_quote from urllib.parse import urlencode as url_encode, quote_plus from urllib.request import urlopen as url_open else: import urlparse from urllib import quote_plus from urllib import quote as url_quote from urllib import unquote as url_unquote from urllib import urlencode as url_encode from urllib2 import urlopen as url_open if PY3: # pragma: no cover def reraise(exc_info): etype, exc, tb = exc_info if exc.__traceback__ is not tb: raise exc.with_traceback(tb) raise exc else: # pragma: no cover exec("def reraise(exc): raise exc[0], exc[1], exc[2]") if PY3: # pragma: no cover def iteritems_(d): return d.items() def itervalues_(d): return d.values() else: def iteritems_(d): return d.iteritems() def itervalues_(d): return d.itervalues() if PY3: # pragma: no cover def unquote(string): if not string: return b'' res = string.split(b'%') if len(res) != 1: string = res[0] for item in res[1:]: try: string += bytes([int(item[:2], 16)]) + item[2:] except ValueError: string += b'%' + item return string def url_unquote(s): return unquote(s.encode('ascii')).decode('latin-1') def parse_qsl_text(qs, encoding='utf-8'): qs = qs.encode('latin-1') qs = qs.replace(b'+', b' ') pairs = [s2 for s1 in qs.split(b'&') for s2 in s1.split(b';') if s2] for name_value in pairs: nv = name_value.split(b'=', 1) if len(nv) != 2: nv.append('') name = unquote(nv[0]) value = unquote(nv[1]) yield (name.decode(encoding), value.decode(encoding)) else: from urlparse import parse_qsl def parse_qsl_text(qs, encoding='utf-8'): qsl = parse_qsl( qs, keep_blank_values=True, strict_parsing=False ) for (x, y) in qsl: yield (x.decode(encoding), y.decode(encoding)) if PY3: # pragma no cover from html import escape else: from cgi import escape WebOb-1.3.1/webob/static.py0000664000175000017500000001301212023145137016242 0ustar chrismchrism00000000000000import mimetypes import os from webob import exc from webob.dec import wsgify from webob.response import Response __all__ = [ 'FileApp', 'DirectoryApp', ] mimetypes._winreg = None # do not load mimetypes from windows registry mimetypes.add_type('text/javascript', '.js') # stdlib default is application/x-javascript mimetypes.add_type('image/x-icon', '.ico') # not among defaults BLOCK_SIZE = 1<<16 class FileApp(object): """An application that will send the file at the given filename. Adds a mime type based on `mimetypes.guess_type()`. """ def __init__(self, filename, **kw): self.filename = filename content_type, content_encoding = mimetypes.guess_type(filename) kw.setdefault('content_type', content_type) kw.setdefault('content_encoding', content_encoding) kw.setdefault('accept_ranges', 'bytes') self.kw = kw # Used for testing purpose self._open = open @wsgify def __call__(self, req): if req.method not in ('GET', 'HEAD'): return exc.HTTPMethodNotAllowed("You cannot %s a file" % req.method) try: stat = os.stat(self.filename) except (IOError, OSError) as e: msg = "Can't open %r: %s" % (self.filename, e) return exc.HTTPNotFound(comment=msg) try: file = self._open(self.filename, 'rb') except (IOError, OSError) as e: msg = "You are not permitted to view this file (%s)" % e return exc.HTTPForbidden(msg) if 'wsgi.file_wrapper' in req.environ: app_iter = req.environ['wsgi.file_wrapper'](file, BLOCK_SIZE) else: app_iter = FileIter(file) return Response( app_iter = app_iter, content_length = stat.st_size, last_modified = stat.st_mtime, #@@ etag **self.kw ).conditional_response_app class FileIter(object): def __init__(self, file): self.file = file def app_iter_range(self, seek=None, limit=None, block_size=None): """Iter over the content of the file. You can set the `seek` parameter to read the file starting from a specific position. You can set the `limit` parameter to read the file up to specific position. Finally, you can change the number of bytes read at once by setting the `block_size` parameter. """ if block_size is None: block_size = BLOCK_SIZE if seek: self.file.seek(seek) if limit is not None: limit -= seek try: while True: data = self.file.read(min(block_size, limit) if limit is not None else block_size) if not data: return yield data if limit is not None: limit -= len(data) if limit <= 0: return finally: self.file.close() __iter__ = app_iter_range class DirectoryApp(object): """An application that serves up the files in a given directory. This will serve index files (by default ``index.html``), or set ``index_page=None`` to disable this. If you set ``hide_index_with_redirect=True`` (it defaults to False) then requests to, e.g., ``/index.html`` will be redirected to ``/``. To customize `FileApp` instances creation (which is what actually serves the responses), override the `make_fileapp` method. """ def __init__(self, path, index_page='index.html', hide_index_with_redirect=False, **kw): self.path = os.path.abspath(path) if not self.path.endswith(os.path.sep): self.path += os.path.sep if not os.path.isdir(self.path): raise IOError( "Path does not exist or is not directory: %r" % self.path) self.index_page = index_page self.hide_index_with_redirect = hide_index_with_redirect self.fileapp_kw = kw def make_fileapp(self, path): return FileApp(path, **self.fileapp_kw) @wsgify def __call__(self, req): path = os.path.abspath(os.path.join(self.path, req.path_info.lstrip('/'))) if os.path.isdir(path) and self.index_page: return self.index(req, path) if (self.index_page and self.hide_index_with_redirect and path.endswith(os.path.sep + self.index_page)): new_url = req.path_url.rsplit('/', 1)[0] new_url += '/' if req.query_string: new_url += '?' + req.query_string return Response( status=301, location=new_url) if not os.path.isfile(path): return exc.HTTPNotFound(comment=path) elif not path.startswith(self.path): return exc.HTTPForbidden() else: return self.make_fileapp(path) def index(self, req, path): index_path = os.path.join(path, self.index_page) if not os.path.isfile(index_path): return exc.HTTPNotFound(comment=index_path) if not req.path_info.endswith('/'): url = req.path_url + '/' if req.query_string: url += '?' + req.query_string return Response( status=301, location=url) return self.make_fileapp(index_path) WebOb-1.3.1/webob/dec.py0000644000175000017500000002371011712015463015514 0ustar chrismchrism00000000000000""" Decorators to wrap functions to make them WSGI applications. The main decorator :class:`wsgify` turns a function into a WSGI application (while also allowing normal calling of the method with an instantiated request). """ from webob.compat import ( bytes_, text_type, ) from webob.request import Request from webob.exc import HTTPException __all__ = ['wsgify'] class wsgify(object): """Turns a request-taking, response-returning function into a WSGI app You can use this like:: @wsgify def myfunc(req): return webob.Response('hey there') With that ``myfunc`` will be a WSGI application, callable like ``app_iter = myfunc(environ, start_response)``. You can also call it like normal, e.g., ``resp = myfunc(req)``. (You can also wrap methods, like ``def myfunc(self, req)``.) If you raise exceptions from :mod:`webob.exc` they will be turned into WSGI responses. There are also several parameters you can use to customize the decorator. Most notably, you can use a :class:`webob.Request` subclass, like:: class MyRequest(webob.Request): @property def is_local(self): return self.remote_addr == '127.0.0.1' @wsgify(RequestClass=MyRequest) def myfunc(req): if req.is_local: return Response('hi!') else: raise webob.exc.HTTPForbidden Another customization you can add is to add `args` (positional arguments) or `kwargs` (of course, keyword arguments). While generally not that useful, you can use this to create multiple WSGI apps from one function, like:: import simplejson def serve_json(req, json_obj): return Response(json.dumps(json_obj), content_type='application/json') serve_ob1 = wsgify(serve_json, args=(ob1,)) serve_ob2 = wsgify(serve_json, args=(ob2,)) You can return several things from a function: * A :class:`webob.Response` object (or subclass) * *Any* WSGI application * None, and then ``req.response`` will be used (a pre-instantiated Response object) * A string, which will be written to ``req.response`` and then that response will be used. * Raise an exception from :mod:`webob.exc` Also see :func:`wsgify.middleware` for a way to make middleware. You can also subclass this decorator; the most useful things to do in a subclass would be to change `RequestClass` or override `call_func` (e.g., to add ``req.urlvars`` as keyword arguments to the function). """ RequestClass = Request def __init__(self, func=None, RequestClass=None, args=(), kwargs=None, middleware_wraps=None): self.func = func if (RequestClass is not None and RequestClass is not self.RequestClass): self.RequestClass = RequestClass self.args = tuple(args) if kwargs is None: kwargs = {} self.kwargs = kwargs self.middleware_wraps = middleware_wraps def __repr__(self): return '<%s at %s wrapping %r>' % (self.__class__.__name__, id(self), self.func) def __get__(self, obj, type=None): # This handles wrapping methods if hasattr(self.func, '__get__'): return self.clone(self.func.__get__(obj, type)) else: return self def __call__(self, req, *args, **kw): """Call this as a WSGI application or with a request""" func = self.func if func is None: if args or kw: raise TypeError( "Unbound %s can only be called with the function it " "will wrap" % self.__class__.__name__) func = req return self.clone(func) if isinstance(req, dict): if len(args) != 1 or kw: raise TypeError( "Calling %r as a WSGI app with the wrong signature") environ = req start_response = args[0] req = self.RequestClass(environ) req.response = req.ResponseClass() try: args = self.args if self.middleware_wraps: args = (self.middleware_wraps,) + args resp = self.call_func(req, *args, **self.kwargs) except HTTPException as exc: resp = exc if resp is None: ## FIXME: I'm not sure what this should be? resp = req.response if isinstance(resp, text_type): resp = bytes_(resp, req.charset) if isinstance(resp, bytes): body = resp resp = req.response resp.write(body) if resp is not req.response: resp = req.response.merge_cookies(resp) return resp(environ, start_response) else: if self.middleware_wraps: args = (self.middleware_wraps,) + args return self.func(req, *args, **kw) def get(self, url, **kw): """Run a GET request on this application, returning a Response. This creates a request object using the given URL, and any other keyword arguments are set on the request object (e.g., ``last_modified=datetime.now()``). :: resp = myapp.get('/article?id=10') """ kw.setdefault('method', 'GET') req = self.RequestClass.blank(url, **kw) return self(req) def post(self, url, POST=None, **kw): """Run a POST request on this application, returning a Response. The second argument (`POST`) can be the request body (a string), or a dictionary or list of two-tuples, that give the POST body. :: resp = myapp.post('/article/new', dict(title='My Day', content='I ate a sandwich')) """ kw.setdefault('method', 'POST') req = self.RequestClass.blank(url, POST=POST, **kw) return self(req) def request(self, url, **kw): """Run a request on this application, returning a Response. This can be used for DELETE, PUT, etc requests. E.g.:: resp = myapp.request('/article/1', method='PUT', body='New article') """ req = self.RequestClass.blank(url, **kw) return self(req) def call_func(self, req, *args, **kwargs): """Call the wrapped function; override this in a subclass to change how the function is called.""" return self.func(req, *args, **kwargs) def clone(self, func=None, **kw): """Creates a copy/clone of this object, but with some parameters rebound """ kwargs = {} if func is not None: kwargs['func'] = func if self.RequestClass is not self.__class__.RequestClass: kwargs['RequestClass'] = self.RequestClass if self.args: kwargs['args'] = self.args if self.kwargs: kwargs['kwargs'] = self.kwargs kwargs.update(kw) return self.__class__(**kwargs) # To match @decorator: @property def undecorated(self): return self.func @classmethod def middleware(cls, middle_func=None, app=None, **kw): """Creates middleware Use this like:: @wsgify.middleware def restrict_ip(app, req, ips): if req.remote_addr not in ips: raise webob.exc.HTTPForbidden('Bad IP: %s' % req.remote_addr) return app @wsgify def app(req): return 'hi' wrapped = restrict_ip(app, ips=['127.0.0.1']) Or if you want to write output-rewriting middleware:: @wsgify.middleware def all_caps(app, req): resp = req.get_response(app) resp.body = resp.body.upper() return resp wrapped = all_caps(app) Note that you must call ``req.get_response(app)`` to get a WebOb response object. If you are not modifying the output, you can just return the app. As you can see, this method doesn't actually create an application, but creates "middleware" that can be bound to an application, along with "configuration" (that is, any other keyword arguments you pass when binding the application). """ if middle_func is None: return _UnboundMiddleware(cls, app, kw) if app is None: return _MiddlewareFactory(cls, middle_func, kw) return cls(middle_func, middleware_wraps=app, kwargs=kw) class _UnboundMiddleware(object): """A `wsgify.middleware` invocation that has not yet wrapped a middleware function; the intermediate object when you do something like ``@wsgify.middleware(RequestClass=Foo)`` """ def __init__(self, wrapper_class, app, kw): self.wrapper_class = wrapper_class self.app = app self.kw = kw def __repr__(self): return '<%s at %s wrapping %r>' % (self.__class__.__name__, id(self), self.app) def __call__(self, func, app=None): if app is None: app = self.app return self.wrapper_class.middleware(func, app=app, **self.kw) class _MiddlewareFactory(object): """A middleware that has not yet been bound to an application or configured. """ def __init__(self, wrapper_class, middleware, kw): self.wrapper_class = wrapper_class self.middleware = middleware self.kw = kw def __repr__(self): return '<%s at %s wrapping %r>' % (self.__class__.__name__, id(self), self.middleware) def __call__(self, app, **config): kw = self.kw.copy() kw.update(config) return self.wrapper_class.middleware(self.middleware, app, **kw) WebOb-1.3.1/webob/datetime_utils.py0000664000175000017500000000477012123261311017774 0ustar chrismchrism00000000000000import calendar from datetime import ( date, datetime, timedelta, tzinfo, ) from email.utils import ( formatdate, mktime_tz, parsedate_tz, ) import time from webob.compat import ( integer_types, long, native_, text_type, ) __all__ = [ 'UTC', 'timedelta_to_seconds', 'year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'parse_date', 'serialize_date', 'parse_date_delta', 'serialize_date_delta', ] _now = datetime.now # hook point for unit tests class _UTC(tzinfo): def dst(self, dt): return timedelta(0) def utcoffset(self, dt): return timedelta(0) def tzname(self, dt): return 'UTC' def __repr__(self): return 'UTC' UTC = _UTC() def timedelta_to_seconds(td): """ Converts a timedelta instance to seconds. """ return td.seconds + (td.days*24*60*60) day = timedelta(days=1) week = timedelta(weeks=1) hour = timedelta(hours=1) minute = timedelta(minutes=1) second = timedelta(seconds=1) # Estimate, I know; good enough for expirations month = timedelta(days=30) year = timedelta(days=365) def parse_date(value): if not value: return None try: value = native_(value) except: return None t = parsedate_tz(value) if t is None: # Could not parse return None if t[-1] is None: # No timezone given. None would mean local time, but we'll force UTC t = t[:9] + (0,) t = mktime_tz(t) return datetime.fromtimestamp(t, UTC) def serialize_date(dt): if isinstance(dt, (bytes, text_type)): return native_(dt) if isinstance(dt, timedelta): dt = _now() + dt if isinstance(dt, (datetime, date)): dt = dt.timetuple() if isinstance(dt, (tuple, time.struct_time)): dt = calendar.timegm(dt) if not (isinstance(dt, float) or isinstance(dt, integer_types)): raise ValueError( "You must pass in a datetime, date, time tuple, or integer object, " "not %r" % dt) return formatdate(dt, usegmt=True) def parse_date_delta(value): """ like parse_date, but also handle delta seconds """ if not value: return None try: value = int(value) except ValueError: return parse_date(value) else: return _now() + timedelta(seconds=value) def serialize_date_delta(value): if isinstance(value, (float, int, long)): return str(int(value)) else: return serialize_date(value) WebOb-1.3.1/toxfast.ini0000664000175000017500000000015412044266064015505 0ustar chrismchrism00000000000000[tox] envlist = py26,py32 [testenv] commands = python setup.py dev python setup.py nosetests WebOb-1.3.1/PKG-INFO0000664000175000017500000000325512252637046014421 0ustar chrismchrism00000000000000Metadata-Version: 1.1 Name: WebOb Version: 1.3.1 Summary: WSGI request and response object Home-page: http://webob.org/ Author: Pylons Project Author-email: ianb@colorstudy.com License: MIT Description: WebOb provides wrappers around the WSGI request environment, and an object to help create WSGI responses. The objects map much of the specified behavior of HTTP, including header parsing and accessors for other standard parts of the environment. You may install the `in-development version of WebOb `_ with ``pip install WebOb==dev`` (or ``easy_install WebOb==dev``). * `WebOb reference `_ * `Bug tracker `_ * `Browse source code `_ * `Mailing list `_ * `Release news `_ * `Detailed changelog `_ Keywords: wsgi request web http Platform: UNKNOWN Classifier: Development Status :: 6 - Mature Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Topic :: Internet :: WWW/HTTP :: WSGI Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3.2 Classifier: Programming Language :: Python :: 3.3 WebOb-1.3.1/docs/0000775000175000017500000000000012252637046014247 5ustar chrismchrism00000000000000WebOb-1.3.1/docs/file-example.txt0000644000175000017500000001674611743245714017375 0ustar chrismchrism00000000000000WebOb File-Serving Example ========================== This document shows how you can make a static-file-serving application using WebOb. We'll quickly build this up from minimal functionality to a high-quality file serving application. .. note:: Starting from 1.2b4, WebOb ships with a :mod:`webob.static` module which implements a :class:`webob.static.FileApp` WSGI application similar to the one described below. This document stays as a didactic example how to serve files with WebOb, but you should consider using applications from :mod:`webob.static` in production. .. comment: >>> import webob, os >>> base_dir = os.path.dirname(os.path.dirname(webob.__file__)) >>> doc_dir = os.path.join(base_dir, 'docs') >>> from doctest import ELLIPSIS First we'll setup a really simple shim around our application, which we can use as we improve our application: .. code-block:: python >>> from webob import Request, Response >>> import os >>> class FileApp(object): ... def __init__(self, filename): ... self.filename = filename ... def __call__(self, environ, start_response): ... res = make_response(self.filename) ... return res(environ, start_response) >>> import mimetypes >>> def get_mimetype(filename): ... type, encoding = mimetypes.guess_type(filename) ... # We'll ignore encoding, even though we shouldn't really ... return type or 'application/octet-stream' Now we can make different definitions of ``make_response``. The simplest version: .. code-block:: python >>> def make_response(filename): ... res = Response(content_type=get_mimetype(filename)) ... res.body = open(filename, 'rb').read() ... return res Let's give it a go. We'll test it out with a file ``test-file.txt`` in the WebOb doc directory: .. code-block:: python >>> fn = os.path.join(doc_dir, 'test-file.txt') >>> open(fn).read() 'This is a test. Hello test people!' >>> app = FileApp(fn) >>> req = Request.blank('/') >>> print req.get_response(app) 200 OK Content-Type: text/plain; charset=UTF-8 Content-Length: 35 This is a test. Hello test people! Well, that worked. But it's not a very fancy object. First, it reads everything into memory, and that's bad. We'll create an iterator instead: .. code-block:: python >>> class FileIterable(object): ... def __init__(self, filename): ... self.filename = filename ... def __iter__(self): ... return FileIterator(self.filename) >>> class FileIterator(object): ... chunk_size = 4096 ... def __init__(self, filename): ... self.filename = filename ... self.fileobj = open(self.filename, 'rb') ... def __iter__(self): ... return self ... def next(self): ... chunk = self.fileobj.read(self.chunk_size) ... if not chunk: ... raise StopIteration ... return chunk ... __next__ = next # py3 compat >>> def make_response(filename): ... res = Response(content_type=get_mimetype(filename)) ... res.app_iter = FileIterable(filename) ... res.content_length = os.path.getsize(filename) ... return res And testing: .. code-block:: python >>> req = Request.blank('/') >>> print req.get_response(app) 200 OK Content-Type: text/plain; charset=UTF-8 Content-Length: 35 This is a test. Hello test people! Well, that doesn't *look* different, but lets *imagine* that it's different because we know we changed some code. Now to add some basic metadata to the response: .. code-block:: python >>> def make_response(filename): ... res = Response(content_type=get_mimetype(filename), ... conditional_response=True) ... res.app_iter = FileIterable(filename) ... res.content_length = os.path.getsize(filename) ... res.last_modified = os.path.getmtime(filename) ... res.etag = '%s-%s-%s' % (os.path.getmtime(filename), ... os.path.getsize(filename), hash(filename)) ... return res Now, with ``conditional_response`` on, and with ``last_modified`` and ``etag`` set, we can do conditional requests: .. code-block:: python >>> req = Request.blank('/') >>> res = req.get_response(app) >>> print res 200 OK Content-Type: text/plain; charset=UTF-8 Content-Length: 35 Last-Modified: ... GMT ETag: ...-... This is a test. Hello test people! >>> req2 = Request.blank('/') >>> req2.if_none_match = res.etag >>> req2.get_response(app) >>> req3 = Request.blank('/') >>> req3.if_modified_since = res.last_modified >>> req3.get_response(app) We can even do Range requests, but it will currently involve iterating through the file unnecessarily. When there's a range request (and you set ``conditional_response=True``) the application will satisfy that request. But with an arbitrary iterator the only way to do that is to run through the beginning of the iterator until you get to the chunk that the client asked for. We can do better because we can use ``fileobj.seek(pos)`` to move around the file much more efficiently. So we'll add an extra method, ``app_iter_range``, that ``Response`` looks for: .. code-block:: python >>> class FileIterable(object): ... def __init__(self, filename, start=None, stop=None): ... self.filename = filename ... self.start = start ... self.stop = stop ... def __iter__(self): ... return FileIterator(self.filename, self.start, self.stop) ... def app_iter_range(self, start, stop): ... return self.__class__(self.filename, start, stop) >>> class FileIterator(object): ... chunk_size = 4096 ... def __init__(self, filename, start, stop): ... self.filename = filename ... self.fileobj = open(self.filename, 'rb') ... if start: ... self.fileobj.seek(start) ... if stop is not None: ... self.length = stop - start ... else: ... self.length = None ... def __iter__(self): ... return self ... def next(self): ... if self.length is not None and self.length <= 0: ... raise StopIteration ... chunk = self.fileobj.read(self.chunk_size) ... if not chunk: ... raise StopIteration ... if self.length is not None: ... self.length -= len(chunk) ... if self.length < 0: ... # Chop off the extra: ... chunk = chunk[:self.length] ... return chunk ... __next__ = next # py3 compat Now we'll test it out: .. code-block:: python >>> req = Request.blank('/') >>> res = req.get_response(app) >>> req2 = Request.blank('/') >>> # Re-fetch the first 5 bytes: >>> req2.range = (0, 5) >>> res2 = req2.get_response(app) >>> res2 >>> # Let's check it's our custom class: >>> res2.app_iter >>> res2.body 'This ' >>> # Now, conditional range support: >>> req3 = Request.blank('/') >>> req3.if_range = res.etag >>> req3.range = (0, 5) >>> req3.get_response(app) >>> req3.if_range = 'invalid-etag' >>> req3.get_response(app) WebOb-1.3.1/docs/news.txt0000664000175000017500000011362012252634076015767 0ustar chrismchrism00000000000000News ==== 1.3.1 (2013-12-13) ------------------ Bug Fixes ~~~~~~~~~ - Fix a bug in SignedCookieProfile whereby we didn't keep the original serializer around, this would cause us to have SignedSerializer be added on top of a SignedSerializer which would cause it to be run twice when attempting to verify a cookie. See https://github.com/Pylons/webob/pull/127 Backwards Incompatibilities ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - When ``CookieProfile.get_value`` and ``SignedCookieProfile.get_value`` fails to deserialize a badly encoded value, we now return ``None`` as if the cookie was never set in the first place instead of allowing a ``ValueError`` to be raised to the calling code. See https://github.com/Pylons/webob/pull/126 1.3 (2013-12-10) ---------------- Features ~~~~~~~~ - Added a read-only ``domain`` property to ``BaseRequest``. This property returns the domain portion of the host value. For example, if the environment contains an ``HTTP_HOST`` value of ``foo.example.com:8000``, ``request.domain`` will return ``foo.example.com``. - Added five new APIs: ``webob.cookies.CookieProfile``, ``webob.cookies.SignedCookieProfile``, ``webob.cookies.JSONSerializer`` and ``webob.cookies.SignedSerializer``, and ``webob.cookies.make_cookie``. These APIs are convenience APIs for generating and parsing cookie headers as well as dealing with signing cookies. - Cookies generated via webob.cookies quoted characters in cookie values that did not need to be quoted per RFC 6265. The following characters are no longer quoted in cookie values: ``~/=<>()[]{}?@`` . The full set of non-letter-or-digit unquoted cookie value characters is now ``!#$%&'*+-.^_`|~/: =<>()[]{}?@``. See http://tools.ietf.org/html/rfc6265#section-4.1.1 for more information. - Cookie names are now restricted to the set of characters expected by RFC 6265. Previously they could contain unsupported characters such as ``/``. - Older versions of Webob escaped the doublequote to ``\"`` and the backslash to ``\\`` when quoting cookie values. Now, instead, cookie serialization generates ``\042`` for the doublequote and ``\134`` for the backslash. This is what is expected as per RFC 6265. Note that old cookie values that do have the older style quoting in them will still be unquoted correctly, however. - Added support for draft status code 451 ("Unavailable for Legal Reasons"). See http://tools.ietf.org/html/draft-tbray-http-legally-restricted-status-00 - Added status codes 428, 429, 431 and 511 to ``util.status_reasons`` (they were already present in a previous release as ``webob.exc`` exceptions). Bug Fixes ~~~~~~~~~ - MIMEAccept happily parsed malformed wildcard strings like "image/pn*" at parse time, but then threw an AssertionError during matching. See https://github.com/Pylons/webob/pull/83 . - Preserve document ordering of GET and POST request data when POST data passed to Request.blank is a MultiDict. See https://github.com/Pylons/webob/pull/96 - Allow query strings attached to PATCH requests to populate request.params. See https://github.com/Pylons/webob/pull/106 - Added Python 3.3 trove classifier. 1.2.3 ------------ * Maintainership transferred to `Pylons Project ` * Fix parsing of form submissions where fields have transfer-content-encoding headers. 1.2.2 ------------ * Fix multiple calls to ``cache_expires()`` not fully overriding the previously set headers. * Fix parsing of form submissions where fields have different encodings. 1.2.1 ------------ * Add index page (e.g., ``index.html``) support for :class:`webob.static.DirectoryApp`. * Detect mime-type when creating a test request with file uploads (``Request.blank("/", POST=dict(file1=("foo.jpg", "xxx")))``) * Relax parsing of ``Accept`` and ``Range`` headers to allow uppercase and extra whitespace. * Fix docs references to some deprecated classes. 1.2 ------------ * Fix :mod:`webob.client` handling of connection-refused on Windows. * Use ``simplejson`` in :mod:`webob.request` if present. * Fix ``resp.retry_after = `` interpreting value as a UNIX timestamp (should interpret as time delta in seconds). 1.2rc1 ------------ * Add ``Response.json`` and ``Request.json`` which reads and sets the body using a JSON encoding (previously only the readable attribute ``Request.json_body`` existed). ``Request.json_body`` is still available as an alias. * Rename ``Response.status_int`` to ``Response.status_code`` (the ``.status_int`` name is still available and will be supported indefinitely). * Add ``Request.text``, the unicode version of the request body (similar to ``Response.text``). * Add :mod:`webob.client` which contains the WSGI application ``send_request_app`` and ``SendRequest``. All requests sent to this application are turned into HTTP requests. * Renamed ``Request.get_response(app)`` to ``Request.send(app)``. The ``.get_response()`` name is still available. * Use ``send_request_app`` as the default application for ``Request.send()``, so you can do: ``resp = Request.blank("http://python.org").send()`` * Add :mod:`webob.static` which contains two new WSGI applications, :class:`FileApp` serve one static file and :class:`DirectoryApp` to serve the content of a directory. They should provide a reusable implementation of :doc:`file-example`. It also comes with support for ``wsgi.file_wrapper``. The implementation has been imported and simplified from :mod:`PasteOb.fileapp`. * Add ``dev`` and ``docs`` setup.py aliases (to install development and docs dependencies respectively, e.g. "python setup.py dev"). 1.2b3 ------------ * Added ``request.host_port`` API (returns port number implied by HTTP_HOST, falling back to SERVER_PORT). * Added ``request.client_addr`` API (returns IP address implied by HTTP_X_FORWARDED_FOR, falling back to REMOTE_ADDR). * Fix corner-case ``response.status_int`` and ``response.status`` mutation bug on py3 (use explicit floor division). * Backwards incompatibility: Request and BaseRequest objects now return Unicode for ``request.path_info`` and ``request.script_name`` under Python 2. Rationale: the legacy behavior of returning the respective raw environ values was nonsensical on Python 3. Working with non-ascii encoded environ variables as raw WSGI values under Python 3 makes no sense, as PEP 3333 specifies that environ variables are bytes-tunneled-as-latin-1 strings. If you don't care about Python 3, and you need strict backwards compatibility, to get legacy behavior of returning bytes on Python 2 for these attributes, use ``webob.LegacyRequest`` instead of ``webob.Request``. Although it's possible to use ``webob.LegacyRequest`` under Python 3, it makes no sense, and it should not be used there. * The above backwards incompatibility fixed nonsensical behavior of ``request.host_url``, ``request.application_url``, ``request.path_url``, ``request.path``, ``request.path_qs``, ``request.url``, ``request.relative_url``, ``request.path_info_peek``, ``request.path_info_pop`` under Python 3. These methods previously dealt with raw SCRIPT_NAME and PATH_INFO values, which caused nonsensical results. * The WebOb Request object now respects an additional WSGI environment variable: ``webob.url_encoding``. ``webob.url_encoding`` will be used to decode the raw WSGI PATH_INFO and SCRIPT_NAME variables when the ``request.path_info`` and ``request.script_name`` APIs are used. * Request objects now accept an additional constructor parameter: ``url_encoding``. ``url_encoding`` will be used to decode PATH_INFO and SCRIPT_NAME from its WSGI-encoded values. If ``webob.url_encoding`` is not set in the environ and ``url_encoding`` is not passed to the Request constructor, the default value ``utf-8`` will be used to decode the PATH_INFO and SCRIPT_NAME. Note that passing ``url_encoding`` will cause the WSGI environment variable ``webob.url_encoding`` to be set. * Fix ``webob.response._request_uri`` internal function to generate sensible request URI under Python 3. This fixed a problem under Python 3 if you were using non-absolute Location headers in responses. 1.2b2 ------ * Fix ``request.cookies.get('name', 'default')``. Previously ``default`` was ignored. 1.2b1 --------- * Mutating the ``request.cookies`` property now reflects the mutations into the ``HTTP_COOKIES`` environ header. * ``Response.etag = (tag, False)`` sets weak etag. * ``Range`` only parses single range now. * ``Range.satisfiable(..)`` is gone. * ``Accept.best_matches()`` is gone; use ``list(request.accept)`` or ``request.accept.best_match(..)`` instead (applies to all Accept-* headers) or similar with ``request.accept_language``. * ``Response.request`` and ``Response.environ`` attrs are undeprecated and no longer raise exceptions when used. These can also be passed to the Response constructor. This is to support codebases that pass them to the constructor or assign them to a response instance. However, some behavior differences from 1.1 exist. In particular, synchronization is no longer done between environ and request attribute properties of Response; you may pass either to the constructor (or both) or assign one or the other or both, but they wont be managed specially and will remain the same over the lifetime of the response just as you passed them. Default values for both ``request`` and ``environ`` on any given response are ``None`` now. * Undeprecated ``uscript_name`` and ``upath_info``. * For backwards compatibility purposes, switch ``req.script_name`` and ``path_info`` back again to contain "raw" undecoded native strings rather than text. Use ``uscript_name`` and ``upath_info`` to get the text version of SCRIPT_NAME and PATH_INFO. * Don't raise an exception if ``unicode_errors`` or ``decode_param_names`` is passed to the Request constructor. Instead, emit a warning. For benefit of Pylons 1.X, which passes both. * Don't raise an exception if HTTPException.exception is used; instead emit a warning. For benefit of Pylons 1.X, which uses it. 1.2a2 --------- * ``req.script_name`` and ``path_info`` now contain text, not bytes. * Deprecated ``uscript_name`` and ``upath_info``. * ``charset`` argument to ``Request`` as well as the attribute can only be set to UTF-8 or the value already present in the ``Content-Type`` header. * ``unicode_errors`` attribute of ``Request`` and related functionality is gone. * To process requests that come in an encoding different from UTF-8, the request needs to be transcoded like this: ``req = req.decode('windows-1251')`` * Added support for weak ETag matching in conditional responses. * Most of etag-related functionality was refactored. 1.2a1 --------- * Python 3.2 compatibility. * No longer compatible with Python 2.5 (only 2.6, 2.7, and 3.2 are supported). * Switched VCS from Mercurial to Git * Moved development to `GitHub `_ * Added full history from PyCon 2011 sprint to the repository * Change ``LimitedLengthFile`` and ``FakeCGIBody`` to inherit from ``io.RawIOBase`` and benefit from ``io.BufferedReader``. * Do not set ``resp.request`` in ``req.get_response(app)`` * ``Response.request`` and ``.environ`` attrs are deprecated and raise exceptions when used. * Deprecated request attributes ``str_GET``, ``str_POST``, ``str_cookies`` and ``str_params`` now raise exceptions when touched. * Remove testing dependency on WebTest. * Remove UnicodeMultiDict class; the result of ``Request.GET`` and ``Request.POST`` is now just a plain ``MultiDict``. * The ``decode_param_names`` Request constructor argument has been removed, along with the ``Request.decode_param_names`` attribute. * The ``Request.as_string()`` method is now better known as ``Request.as_bytes()``. * The ``Request.from_string()`` method is now better known as ``Request.from_bytes()``. * A new method named ``Request.as_text()`` now exists. * A new method named ``Request.from_text()`` now exists. * The ``webob.dec.wsgify`` repr() is now much less informative, but a lot easier to test and maintain. 1.1.1 --------- * Fix disconnect detection being incorrect in some cases (`issue 21 `_). * Fix exception when calling ``.accept.best_match(..)`` on a header containing ``'*'`` (instead of ``'*/*'``). * Extract some of the ``Accept`` code into subclasses (``AcceptCharset``, ``AcceptLanguage``). * Improve language matching so that the app can now offer a generic language code and it will match any of the accepted dialects (``'en' in AcceptLanguage('en-gb')``). * Normalize locale names when matching (``'en_GB' in AcceptLanguage('en-gb')``). * Deprecate ``etag.weak_match(..)``. * Deprecate ``Response.request`` and ``Response.environ`` attrs. 1.1 --------- * Remove deprecation warnings for ``unicode_body`` and ``ubody``. 1.1rc1 --------- * Deprecate ``Response.ubody`` / ``.unicode_body`` in favor of new ``.text`` attribute (the old names will be removed in 1.3 or even later). * Make ``Response.write`` much more efficient (`issue 18 `_). * Make sure copying responses does not reset Content-Length or Content-MD5 of the original (and that of future copies). * Change ``del res.body`` semantics so that it doesn't make the response invalid, but only removes the response body. * Remove ``Response._body`` so the ``_app_iter`` is the only representation. 1.1b2 --------- * Add detection for browser / user-agent disconnects. If the client disconnected before sending the entire request body (POST / PUT), ``req.POST``, ``req.body`` and other related properties and methods will raise an exception. Previously this caused the application get a truncated request with no indication that it is incomplete. * Make ``Response.body_file`` settable. This is now valid: ``Response(body_file=open('foo.bin'), content_type=...)`` * Revert the restriction on req.body not being settable for GET and some other requests. Such requests actually can have a body according to HTTP BIS (see also `commit message `_) * Add support for file upload testing via ``Request.blank(POST=..)``. Patch contributed by Tim Perevezentsev. See also: `ticket `_, `changeset `_. * Deprecate ``req.str_GET``, ``str_POST``, ``str_params`` and ``str_cookies`` (warning). * Deprecate ``req.decode_param_names`` (warning). * Change ``req.decode_param_names`` default to ``True``. This means that ``.POST``, ``.GET``, ``.params`` and ``.cookies`` keys are now unicode. This is necessary for WebOb to behave as close as possible on Python 2 and Python 3. 1.1b1 --------- * We have acquired the webob.org domain, docs are now hosted at `docs.webob.org `_ * Make ``accept.quality(..)`` return best match quality, not first match quality. * Fix ``Range.satisfiable(..)`` edge cases. * Make sure ``WSGIHTTPException`` instances return the same headers for ``HEAD`` and ``GET`` requests. * Drop Python 2.4 support * Deprecate ``HTTPException.exception`` (warning on use). * Deprecate ``accept.first_match(..)`` (warning on use). Use ``.best_match(..)`` instead. * Complete deprecation of ``req.[str_]{post|query}vars`` properties (exception on use). * Remove ``FakeCGIBody.seek`` hack (no longer necessary). 1.0.8 ------ * Escape commas in cookie values (see also: `stdlib Cookie bug `_) * Change cookie serialization to more closely match how cookies usually are serialized (unquoted expires, semicolon separators even between morsels) * Fix some rare cases in cookie parsing * Enhance the req.is_body_readable to always guess GET, HEAD, DELETE and TRACE as unreadable and PUT and POST as readable (`issue 12 `_) * Deny setting req.body or req.body_file to non-empty values for GET, HEAD and other bodiless requests * Fix running nosetests with arguments on UNIX systems (`issue 11 `_) 1.0.7 ------ * Fix ``Accept`` header matching for items with zero-quality (`issue 10 `_) * Hide password values in ``MultiDict.__repr__`` 1.0.6 ------ * Use ``environ['wsgi.input'].read()`` instead of ``.read(-1)`` because the former is explicitly mentioned in PEP-3333 and CherryPy server does not support the latter. * Add new ``environ['webob.is_body_readable']`` flag which specifies if the input stream is readable even if the ``CONTENT_LENGTH`` is not set. WebOb now only ever reads the input stream if the content-length is known or this flag is set. * The two changes above fix a hangup with CherryPy and wsgiref servers (`issue 6 `_) * ``req.body_file`` is now safer to read directly. For ``GET`` and other similar requests it returns an empty ``StringIO`` or ``BytesIO`` object even if the server passed in something else. * Setting ``req.body_file`` to a string now produces a PendingDeprecationWarning. It will produce DeprecationWarning in 1.1 and raise an error in 1.2. Either set ``req.body_file`` to a file-like object or set ``req.body`` to a string value. * Fix ``.pop()`` and ``.setdefault(..)`` methods of ``req/resp.cache_control`` * Thanks to the participants of `Pyramid sprint at the PyCon US 2011 `_ WebOb now has 100% test coverage. 1.0.5 ------ * Restore Python 2.4 compatibility. 1.0.4 ------ * The field names escaping bug semi-fixed in 1.0.3 and originally blamed on cgi module was in fact a ``webob.request._encode_multipart`` bug (also in Google Chrome) and was lurking in webob code for quite some time -- 1.0.2 just made it trigger more often. Now it is fixed properly. * Make sure that req.url and related properties do not unnecessarily escape some chars (``:@&+$``) in the URI path (`issue 5 `_) * Revert some changes from 1.0.3 that have broken backwards compatibility for some apps. Getting ``req.body_file`` does not make input stream seekable, but there's a new property ``req.body_file_seekable`` that does. * ``Request.get_response`` and ``Request.call_application`` seek the input body to start before calling the app (if possible). * Accessing ``req.body`` 'rewinds' the input stream back to pos 0 as well. * When accessing ``req.POST`` we now avoid making the body seekable as the input stream data are preserved in ``FakeCGIBody`` anyway. * Add new method ``Request.from_string``. * Make sure ``Request.as_string()`` uses CRLF to separate headers. * Improve parity between ``Request.as_string()`` and ``.from_file``/``.from_string`` methods, so that the latter can parse output of the former and create a similar request object which wasn't always the case previously. 1.0.3 ------ * Correct a caching issue introduced in WebOb 1.0.2 that was causing unnecessary reparsing of POST requests. * Fix a bug regarding field names escaping for forms submitted as ``multipart/form-data``. For more infromation see `the bug report and discussion `_ and 1.0.4 notes for further fix. * Add ``req.http_version`` attribute. 1.0.2 ------ * Primary maintainer is now `Sergey Schetinin `_. * Issue tracker moved from `Trac `_ to bitbucket's `issue tracker `_ * WebOb 1.0.1 changed the behavior of ``MultiDict.update`` to be more in line with other dict-like objects. We now also issue a warning when we detect that the client code seems to expect the old, extending semantics. * Make ``Response.set_cookie(key, None)`` set the 'delete-cookie' (same as ``.delete_cookie(key)``) * Make ``req.upath_info`` and ``req.uscript_name`` settable * Add :meth:``Request.as_string()`` method * Add a ``req.is_body_seekable`` property * Support for the ``deflate`` method with ``resp.decode_content()`` * To better conform to WSGI spec we no longer attempt to use seek on ``wsgi.input`` file instead we assume it is not seekable unless ``env['webob.is_body_seekable']`` is set. When making the body seekable we set that flag. * A call to ``req.make_body_seekable()`` now guarantees that the body is seekable, is at 0 position and that a correct ``req.content_length`` is present. * ``req.body_file`` is always seekable. To access ``env['wsgi.input']`` without any processing, use ``req.body_file_raw``. (Partially reverted in 1.0.4) * Fix responses to HEAD requests with Range. * Fix ``del resp.content_type``, ``del req.body``, ``del req.cache_control`` * Fix ``resp.merge_cookies()`` when called with an argument that is not a Response instance. * Fix ``resp.content_body = None`` (was removing Cache-Control instead) * Fix ``req.body_file = f`` setting ``CONTENT_LENGTH`` to ``-1`` (now removes from environ) * Fix: make sure req.copy() leaves the original with seekable body * Fix handling of WSGI environs with missing ``SCRIPT_NAME`` * A lot of tests were added by Mariano Mara and Danny Navarro. 1.0.1 ----- * As WebOb requires Python 2.4 or later, drop some compatibility modules and update the code to use the decorator syntax. * Implement optional on-the-fly response compression (``resp.encode_content(lazy=True)``) * Drop ``util.safezip`` module and make ``util`` a module instead of a subpackage. Merge ``statusreasons`` into it. * Instead of using stdlib ``Cookie`` with monkeypatching, add a derived but thoroughly rewritten, cleaner, safer and faster ``webob.cookies`` module. * Fix: ``Response.merge_cookies`` now copies the headers before modification instead of doing it in-place. * Fix: setting request header attribute to ``None`` deletes that header. (Bug only affected the 1.0 release). * Use ``io.BytesIO`` for the request body file on Python 2.7 and newer. * If a UnicodeMultiDict was used as the ``multi`` argument of another UnicodeMultiDict, and a ``cgi.FieldStorage`` with a ``filename`` with high-order characters was present in the underlying UnicodeMultiDict, a ``UnicodeEncodeError`` would be raised when any helper method caused the ``_decode_value`` method to be called, because the method would try to decode an already decoded string. * Fix tests to pass under Python 2.4. * Add descriptive docstrings to each exception in ``webob.exc``. * Change the behaviour of ``MultiDict.update`` to overwrite existing header values instead of adding new headers. The extending semantics are now available via the ``extend`` method. * Fix a bug in ``webob.exc.WSGIHTTPException.__init__``. If a list of ``headers`` was passed as a sequence which contained duplicate keys (for example, multiple ``Set-Cookie`` headers), all but one of those headers would be lost, because the list was effectively flattened into a dictionary as the result of calling ``self.headers.update``. Fixed via calling ``self.headers.extend`` instead. 1.0 --- * 1.0, yay! * Pull in werkzeug Cookie fix for malformed cookie bug. * Implement :meth:`Request.from_file` and :meth:`Response.from_file` which are kind of the inversion of ``str(req)`` and ``str(resp)`` * Add optional ``pattern`` argument to :meth:`Request.path_info_pop` that requires the ``path_info`` segment to match the passed regexp to get popped and returned. * Rewrite most of descriptor implementations for speed. * Reorder descriptor declarations to group them by their semantics. * Move code around so that there are fewer compat modules. * Change :meth:``HTTPError.__str__`` to better conform to PEP 352. * Make :attr:`Request.cache_control` a view on the headers. * Correct Accept-Language and Accept-Charset matching to fully conform to the HTTP spec. * Expose parts of :meth:`Request.blank` as :func:`environ_from_url` and :func:`environ_add_POST` * Fix Authorization header parsing for some corner cases. * Fix an error generated if the user-agent sends a 'Content_Length' header (note the underscore). * Kill :attr:`Request.default_charset`. Request charset defaults to UTF-8. This ensures that all values in ``req.GET``, ``req.POST`` and ``req.params`` are always unicode. * Fix the ``headerlist`` and ``content_type`` constructor arguments priorities for :class:`HTTPError` and subclasses. * Add support for weak etags to conditional Response objects. * Fix locale-dependence for some cookie dates strings. * Improve overall test coverage. * Rename class ``webob.datastruct.EnvironHeaders`` to ``webob.headers.EnvironHeaders`` * Rename class ``webob.headerdict.HeaderDict`` to ``webob.headers.ResponseHeaders`` * Rename class ``webob.updatedict.UpdateDict`` to ``webob.cachecontrol.UpdateDict`` 0.9.8 ----- * Fix issue with WSGIHTTPException inadvertently generating unicode body and failing to encode it * WWW-Authenticate response header is accessible as ``response.www_authenticate`` * ``response.www_authenticate`` and ``request.authorization`` hold None or tuple ``(auth_method, params)`` where ``params`` is a dictionary (or a string when ``auth_method`` is not one of known auth schemes and for Authenticate: Basic ...) * Don't share response headers when getting a response like ``resp = req.get_response(some_app)``; this can avoid some funny errors with modifying headers and reusing Response objects. * Add `overwrite` argument to :meth:`Response.set_cookie` that make the new value overwrite the previously set. `False` by default. * Add `strict` argument to :meth:`Response.unset_cookie` that controls if an exception should be raised in case there are no cookies to unset. `True` by default. * Fix ``req.GET.copy()`` * Make sure that 304 Not Modified responses generated by :meth:`Response.conditional_response_app` exclude Content-{Length/Type} headers * Fix ``Response.copy()`` not being an independent copy * When the requested range is not satisfiable, return a 416 error (was returning entire body) * Truncate response for range requests that go beyond the end of body (was treating as invalid). 0.9.7.1 ------- * Fix an import problem with Pylons 0.9.7 ----- * Moved repository from svn location to http://bitbucket.org/ianb/webob/ * Arguments to :meth:`Accept.best_match` must be specific types, not wildcards. The server should know a list of specic types it can offer and use ``best_match`` to select a specific one. * With ``req.accept.best_match([types])`` prefer the first type in the list (previously it preferred later types). * Also, make sure that if the user-agent accepts multiple types and there are multiple matches to the types that the application offers, ``req.accept.best_match([..])`` returns the most specific match. So if the server can satisfy either ``image/*`` or ``text/plain`` types, the latter will be picked independent from the order the accepted or offered types are listed (given they have the same quality rating). * Fix Range, Content-Range and AppIter support all of which were broken in many ways, incorrectly parsing ranges, reporting incorrect content-ranges, failing to generate the correct body to satisfy the range from ``app_iter`` etc. * Fix assumption that presense of a ``seek`` method means that the stream is seekable. * Add ``ubody`` alias for ``Response.unicode_body`` * Add Unicode versions of ``Request.script_name`` and ``path_info``: ``uscript_name`` and ``upath_info``. * Split __init__.py into four modules: request, response, descriptors and datetime_utils. * Fix ``Response.body`` access resetting Content-Length to zero for HEAD responses. * Support passing Unicode bodies to :class:`WSGIHTTPException` constructors. * Make ``bool(req.accept)`` return ``False`` for requests with missing Accept header. * Add HTTP version to :meth:`Request.__str__` output. * Resolve deprecation warnings for parse_qsl on Python 2.6 and newer. * Fix :meth:`Response.md5_etag` setting Content-MD5 in incorrect format. * Add ``Request.authorization`` property for Authorization header. * Make sure ETag value is always quoted (required by RFC) * Moved most ``Request`` behavior into a new class named ``BaseRequest``. The ``Request`` class is now a superclass for ``BaseRequest`` and a simple mixin which manages ``environ['webob.adhoc_attrs']`` when ``__setitem__``, ``__delitem__`` and ``__getitem__`` are called. This allows framework developers who do not want the ``environ['webob.adhoc_attrs']`` mutation behavior from ``__setattr__``. (chrism) * Added response attribute ``response.content_disposition`` for its associated header. * Changed how ``charset`` is determined on :class:`webob.Request` objects. Now the ``charset`` parameter is read on the Content-Type header, if it is present. Otherwise a ``default_charset`` parameter is read, or the ``charset`` argument to the Request constructor. This is more similar to how :class:`webob.Response` handles the charset. * Made the case of the Content-Type header consistent (note: this might break some doctests). * Make ``req.GET`` settable, such that ``req.environ['QUERY_STRING']`` is updated. * Fix problem with ``req.POST`` causing a re-parse of the body when you instantiate multiple ``Request`` objects over the same environ (e.g., when using middleware that looks at ``req.POST``). * Recreate the request body properly when a ``POST`` includes file uploads. * When ``req.POST`` is updated, the generated body will include the new values. * Added a ``POST`` parameter to :meth:`webob.Request.blank`; when given this will create a request body for the POST parameters (list of two-tuples or dictionary-like object). Note: this does not handle unicode or file uploads. * Added method :meth:`webob.Response.merge_cookies`, which takes the ``Set-Cookie`` headers from a Response, and merges them with another response or WSGI application. (This is useful for flash messages.) * Fix a problem with creating exceptions like ``webob.exc.HTTPNotFound(body='', content_type='application/xml')`` (i.e., non-HTML exceptions). * When a Location header is not absolute in a Response, it will be made absolute when the Response is called as a WSGI application. This makes the response less bound to a specific request. * Added :mod:`webob.dec`, a decorator for making WSGI applications from functions with the signature ``resp = app(req)``. 0.9.6.1 ------- * Fixed :meth:`Response.__init__`, which for some content types would raise an exception. * The ``req.body`` property will not recreate a StringIO object unnecessarily when rereading the body. 0.9.6 ----- * Removed `environ_getter` from :class:`webob.Request`. This largely-unused option allowed a Request object to be instantiated with a dynamic underlying environ. Since it wasn't used much, and might have been ill-advised from the beginning, and affected performance, it has been removed (from Chris McDonough). * Speed ups for :meth:`webob.Response.__init__` and :meth:`webob.Request.__init__` * Fix defaulting of ``CONTENT_TYPE`` instead of ``CONTENT_LENGTH`` to 0 in ``Request.str_POST``. * Added :meth:`webob.Response.copy` 0.9.5 ----- * Fix ``Request.blank('/').copy()`` raising an exception. * Fix a potential memory leak with HEAD requests and 304 responses. * Make :func:`webob.html_escape` respect the ``.__html__()`` magic method, which allows you to use HTML in :class:`webob.exc.HTTPException` instances. * Handle unicode values for ``resp.location``. * Allow arbitrary keyword arguments to ``exc.HTTP*`` (the same keywords you can send to :class:`webob.Response`). * Allow setting :meth:`webob.Response.cache_expires` (usually it is called as a method). This is primarily to allow ``Response(cache_expires=True)``. 0.9.4 ----- * Quiet Python 2.6 deprecation warnings. * Added an attribute ``unicode_errors`` to :class:`webob.Response` -- if set to something like ``unicode_errors='replace'`` it will decode ``resp.body`` appropriately. The default is ``strict`` (which was the former un-overridable behavior). 0.9.3 ----- * Make sure that if changing the body the Content-MD5 header is removed. (Otherwise a lot of middleware would accidentally corrupt responses). * Fixed ``Response.encode_content('identity')`` case (was a no-op even for encoded bodies). * Fixed :meth:`Request.remove_conditional_headers` that was removing If-Match header instead of If-None-Match. * Fixed ``resp.set_cookie(max_age=timedelta(...))`` * ``request.POST`` now supports PUT requests with the appropriate Content-Type. 0.9.2 ----- * Add more arguments to :meth:`Request.remove_conditional_headers` for more fine-grained control: `remove_encoding`, `remove_range`, `remove_match`, `remove_modified`. All of them are `True` by default. * Add an `set_content_md5` argument to :meth:`Response.md5_etag` that calculates and sets Content-MD5 reponse header from current body. * Change formatting of cookie expires, to use the more traditional format ``Wed, 5-May-2001 15:34:10 GMT`` (dashes instead of spaces). Browsers should deal with either format, but some other code expects dashes. * Added in ``sorted`` function for backward compatibility with Python 2.3. * Allow keyword arguments to :class:`webob.Request`, which assign attributes (possibly overwriting values in the environment). * Added methods :meth:`webob.Request.make_body_seekable` and :meth:`webob.Request.copy_body`, which make it easier to share a request body among different consuming applications, doing something like `req.make_body_seekable(); req.body_file.seek(0)` 0.9.1 ----- * ``request.params.copy()`` now returns a writable MultiDict (before it returned an unwritable object). * There were several things broken with ``UnicodeMultiDict`` when ``decode_param_names`` is turned on (when the dictionary keys are unicode). * You can pass keyword arguments to ``Request.blank()`` that will be used to construct ``Request`` (e.g., ``Request.blank('/', decode_param_names=True)``). * If you set headers like ``response.etag`` to a unicode value, they will be encoded as ISO-8859-1 (however, they will remain encoded, and ``response.etag`` will not be a unicode value). * When parsing, interpret times with no timezone as UTC (previously they would be interpreted as local time). * Set the Expires property on cookies when using ``response.set_cookie()``. This is inherited from ``max_age``. * Support Unicode cookie values 0.9 --- * Added ``req.urlarg``, which represents positional arguments in ``environ['wsgiorg.routing_args']``. * For Python 2.4, added attribute get/set proxies on exception objects from, for example, ``webob.exc.HTTPNotFound().exception``, so that they act more like normal response objects (despite not being new-style classes or ``webob.Response`` objects). In Python 2.5 the exceptions are ``webob.Response`` objects. Backward Incompatible Changes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * The ``Response`` constructor has changed: it is now ``Response([body], [status], ...)`` (before it was ``Response([status], [body], ...)``). Body may be str or unicode. * The ``Response`` class defaults to ``text/html`` for the Content-Type, and ``utf8`` for the charset (charset is only set on ``text/*`` and ``application/*+xml`` responses). Bugfixes and Small Changes ~~~~~~~~~~~~~~~~~~~~~~~~~~ * Use ``BaseCookie`` instead of ``SimpleCookie`` for parsing cookies. * Added ``resp.write(text)`` method, which is equivalent to ``resp.body += text`` or ``resp.unicode_body += text``, depending on the type of ``text``. * The ``decode_param_names`` argument (used like ``Request(decode_param_names=True)``) was being ignored. * Unicode decoding of file uploads and file upload filenames were causing errors when decoding non-file-upload fields (both fixes from Ryan Barrett). 0.8.5 ----- * Added response methods ``resp.encode_content()`` and ``resp.decode_content()`` to gzip or ungzip content. * ``Response(status=404)`` now works (before you would have to use ``status="404 Not Found"``). * Bugfix (typo) with reusing POST body. * Added ``226 IM Used`` response status. * Backport of ``string.Template`` included for Python 2.3 compatibility. 0.8.4 ----- * ``__setattr__`` would keep ``Request`` subclasses from having properly settable environ proxies (like ``req.path_info``). 0.8.3 ----- * ``request.POST`` was giving FieldStorage objects for *every* attribute, not just file uploads. This is fixed now. * Added request attributes ``req.server_name`` and ``req.server_port`` for the environ keys ``SERVER_NAME`` and ``SERVER_PORT``. * Avoid exceptions in ``req.content_length``, even if ``environ['CONTENT_LENGTH']`` is somehow invalid. 0.8.2 ----- * Python 2.3 compatibility: backport of ``reversed(seq)`` * Made separate ``.exception`` attribute on ``webob.exc`` objects, since new-style classes can't be raised as exceptions. * Deprecate ``req.postvars`` and ``req.queryvars``, instead using the sole names ``req.GET`` and ``req.POST`` (also ``req.str_GET`` and ``req.str_POST``). The old names give a warning; will give an error in next release, and be completely gone in the following release. * ``req.user_agent`` is now just a simple string (parsing the User-Agent header was just too volatile, and required too much knowledge about current browsers). Similarly, ``req.referer_search_query()`` is gone. * Added parameters ``version`` and ``comment`` to ``Response.set_cookie()``, per William Dode's suggestion. * Was accidentally consuming file uploads, instead of putting the ``FieldStorage`` object directly in the parameters. 0.8.1 ----- * Added ``res.set_cookie(..., httponly=True)`` to set the ``HttpOnly`` attribute on the cookie, which keeps Javascript from reading the cookie. * Added some WebDAV-related responses to ``webob.exc`` * Set default ``Last-Modified`` when using ``response.cache_expire()`` (fixes issue with Opera) * Generally fix ``.cache_control`` 0.8 --- First release. Nothing is new, or everything is new, depending on how you think about it. WebOb-1.3.1/docs/index.txt0000664000175000017500000002761512035562143016124 0ustar chrismchrism00000000000000WebOb +++++ .. toctree:: reference modules/webob modules/dec modules/static differences file-example wiki-example comment-example jsonrpc-example do-it-yourself news license .. contents:: .. comment: >>> from doctest import ELLIPSIS Status & License ================ WebOb is an extraction and refinement of pieces from `Paste `_. It is under active development. Discussion should happen on the `Paste mailing lists `_, and bugs can go on the `issue tracker `_. It was originally written by `Ian Bicking `_, and is being maintained by the `Pylons Project `. WebOb is released under an `MIT-style license `_. WebOb development happens on `GitHub `_. Development version is installable via `easy_install webob==dev `__. You can clone the source code with:: $ git clone https://github.com/Pylons/webob.git Introduction ============ WebOb provides objects for HTTP requests and responses. Specifically it does this by wrapping the `WSGI `_ request environment and response status/headers/app_iter(body). The request and response objects provide many conveniences for parsing HTTP request and forming HTTP responses. Both objects are read/write: as a result, WebOb is also a nice way to create HTTP requests and parse HTTP responses; however, we won't cover that use case in this document. The `reference documentation `_ shows many examples of creating requests. Request ======= The request object is a wrapper around the `WSGI environ dictionary `_. This dictionary contains keys for each header, keys that describe the request (including the path and query string), a file-like object for the request body, and a variety of custom keys. You can always access the environ with ``req.environ``. Some of the most important/interesting attributes of a request object: ``req.method``: The request method, e.g., ``'GET'``, ``'POST'`` ``req.GET``: A `dictionary-like object`_ with all the variables in the query string. ``req.POST``: A `dictionary-like object`_ with all the variables in the request body. This only has variables if the request was a ``POST`` and it is a form submission. ``req.params``: A `dictionary-like object`_ with a combination of everything in ``req.GET`` and ``req.POST``. ``req.body``: The contents of the body of the request. This contains the entire request body as a string. This is useful when the request is a ``POST`` that is *not* a form submission, or a request like a ``PUT``. You can also get ``req.body_file`` for a file-like object. ``req.cookies``: A simple dictionary of all the cookies. ``req.headers``: A dictionary of all the headers. This is dictionary is case-insensitive. ``req.urlvars`` and ``req.urlargs``: ``req.urlvars`` is the keyword parameters associated with the request URL. ``req.urlargs`` are the positional parameters. These are set by products like `Routes `_ and `Selector `_. .. _`dictionary-like object`: #multidict Also, for standard HTTP request headers there are usually attributes, for instance: ``req.accept_language``, ``req.content_length``, ``req.user_agent``, as an example. These properties expose the *parsed* form of each header, for whatever parsing makes sense. For instance, ``req.if_modified_since`` returns a `datetime `_ object (or None if the header is was not provided). Details are in the `Request reference `_. URLs ---- In addition to these attributes, there are several ways to get the URL of the request. I'll show various values for an example URL ``http://localhost/app-root/doc?article_id=10``, where the application is mounted at ``http://localhost/app-root``. ``req.url``: The full request URL, with query string, e.g., ``'http://localhost/app-root/doc?article_id=10'`` ``req.application_url``: The URL of the application (just the SCRIPT_NAME portion of the path, not PATH_INFO). E.g., ``'http://localhost/app-root'`` ``req.host_url``: The URL with the host, e.g., ``'http://localhost'`` ``req.relative_url(url, to_application=False)``: Gives a URL, relative to the current URL. If ``to_application`` is True, then resolves it relative to ``req.application_url``. Methods ------- There are `several methods `_ but only a few you'll use often: ``Request.blank(base_url)``: Creates a new request with blank information, based at the given URL. This can be useful for subrequests and artificial requests. You can also use ``req.copy()`` to copy an existing request, or for subrequests ``req.copy_get()`` which copies the request but always turns it into a GET (which is safer to share for subrequests). ``req.get_response(wsgi_application)``: This method calls the given WSGI application with this request, and returns a `Response`_ object. You can also use this for subrequests or testing. Unicode ------- Many of the properties in the request object will return unicode values if the request encoding/charset is provided. The client *can* indicate the charset with something like ``Content-Type: application/x-www-form-urlencoded; charset=utf8``, but browsers seldom set this. You can set the charset with ``req.charset = 'utf8'``, or during instantiation with ``Request(environ, charset='utf8'). If you subclass ``Request`` you can also set ``charset`` as a class-level attribute. If it is set, then ``req.POST``, ``req.GET``, ``req.params``, and ``req.cookies`` will contain unicode strings. Each has a corresponding ``req.str_*`` (like ``req.str_POST``) that is always ``str`` and never unicode. Response ======== The response object looks a lot like the request object, though with some differences. The request object wraps a single ``environ`` object; the response object has three fundamental parts (based on WSGI): ``response.status``: The response code plus message, like ``'200 OK'``. To set the code without the reason, use ``response.status_code = 200``. ``response.headerlist``: A list of all the headers, like ``[('Content-Type', 'text/html')]``. There's a case-insensitive `dictionary-like object`_ in ``response.headers`` that also allows you to access these same headers. ``response.app_iter``: An iterable (such as a list or generator) that will produce the content of the response. This is also accessible as ``response.body`` (a string), ``response.unicode_body`` (a unicode object, informed by ``response.charset``), and ``response.body_file`` (a file-like object; writing to it appends to ``app_iter``). Everything else in the object derives from this underlying state. Here's the highlights: ``response.content_type``: The content type *not* including the ``charset`` parameter. Typical use: ``response.content_type = 'text/html'``. You can subclass ``Response`` and add a class-level attribute ``default_content_type`` to set this automatically on instantiation. ``response.charset``: The ``charset`` parameter of the content-type, it also informs encoding in ``response.unicode_body``. ``response.content_type_params`` is a dictionary of all the parameters. ``response.request``: This optional attribute can point to the request object associated with this response object. ``response.set_cookie(key, value, max_age=None, path='/', domain=None, secure=None, httponly=False, version=None, comment=None)``: Set a cookie. The keyword arguments control the various cookie parameters. The ``max_age`` argument is the length for the cookie to live in seconds (you may also use a timedelta object). The `Expires`` key will also be set based on the value of ``max_age``. ``response.delete_cookie(key, path='/', domain=None)``: Delete a cookie from the client. This sets ``max_age`` to 0 and the cookie value to ``''``. ``response.cache_expires(seconds=0)``: This makes this response cachable for the given number of seconds, or if ``seconds`` is 0 then the response is uncacheable (this also sets the ``Expires`` header). ``response(environ, start_response)``: The response object is a WSGI application. As an application, it acts according to how you creat it. It *can* do conditional responses if you pass ``conditional_response=True`` when instantiating (or set that attribute later). It can also do HEAD and Range requests. Headers ------- Like the request, most HTTP response headers are available as properties. These are parsed, so you can do things like ``response.last_modified = os.path.getmtime(filename)``. The details are available in the `extracted Response documentation `_. Instantiating the Response -------------------------- Of course most of the time you just want to *make* a response. Generally any attribute of the response can be passed in as a keyword argument to the class; e.g.: .. code-block:: python response = Response(body='hello world!', content_type='text/plain') The status defaults to ``'200 OK'``. The content_type does not default to anything, though if you subclass ``Response`` and set ``default_content_type`` you can override this behavior. Exceptions ========== To facilitate error responses like 404 Not Found, the module ``webob.exc`` contains classes for each kind of error response. These include boring but appropriate error bodies. Each class is named ``webob.exc.HTTP*``, where ``*`` is the reason for the error. For instance, ``webob.exc.HTTPNotFound``. It subclasses ``Response``, so you can manipulate the instances in the same way. A typical example is: .. code-block:: python response = HTTPNotFound('There is no such resource') # or: response = HTTPMovedPermanently(location=new_url) You can use this like: .. code-block:: python try: ... stuff ... raise HTTPNotFound('No such resource') except HTTPException, e: return e(environ, start_response) The exceptions are still WSGI applications, but you cannot set attributes like ``content_type``, ``charset``, etc. on these exception objects. Multidict ========= Several parts of WebOb use a "multidict"; this is a dictionary where a key can have multiple values. The quintessential example is a query string like ``?pref=red&pref=blue``; the ``pref`` variable has two values: ``red`` and ``blue``. In a multidict, when you do ``request.GET['pref']`` you'll get back only ``'blue'`` (the last value of ``pref``). Sometimes returning a string, and sometimes returning a list, is the cause of frequent exceptions. If you want *all* the values back, use ``request.GET.getall('pref')``. If you want to be sure there is *one and only one* value, use ``request.GET.getone('pref')``, which will raise an exception if there is zero or more than one value for ``pref``. When you use operations like ``request.GET.items()`` you'll get back something like ``[('pref', 'red'), ('pref', 'blue')]``. All the key/value pairs will show up. Similarly ``request.GET.keys()`` returns ``['pref', 'pref']``. Multidict is a view on a list of tuples; all the keys are ordered, and all the values are ordered. Example ======= The `file-serving example `_ shows how to do more advanced HTTP techniques, while the `comment middleware example `_ shows middleware. For applications it's more reasonable to use WebOb in the context of a larger framework. `Pylons `_ uses WebOb in 0.9.7+. .. meta:: :google-site-verification: 1oDd59jXPaC0wzgPn3g6cFMI-QvEHjkh8-2rlZeXqwc WebOb-1.3.1/docs/pycon2011/0000775000175000017500000000000012252637046015703 5ustar chrismchrism00000000000000WebOb-1.3.1/docs/pycon2011/pycon-py3k-sprint.txt0000644000175000017500000000323311636566135022001 0ustar chrismchrism00000000000000Python 3 Sprint Outcomes ======================== We provided WebOb with 100% statement coverage at the 2011 PyCon Pyramid sprint in Atlanta GA. Participated: Alexandre Conrad, Patricio Paez, Whit Morriss, Rob Miller, Reed O'Brien, Chris Shenton, Joe Dallago, Tres Seaver, Casey Duncan, Kai Groner, Chris McDonough. In doing so, we added roughly 700-800 unit tests, and disused existing doctests as coverage (they are still runnable, but don't get run during ``setup.py test``). We never did get around to actually doing any porting to Python 3. Adding comprehensive test coverage proved to be enough work to fill the sprint days. The bitbucket fork on which this work was done is at https://bitbucket.org/chrism/webob-py3k. I've made a tag in that repository named "sprint-coverage" which represents a reasonable place to pull from for integration into mainline. Testing Normally ---------------- $ python2.x setup.py test Testing Coverage ---------------- $ python2.X setup.py nosetests --with-coverage Testing Documentation --------------------- Doctests don't run when you run "setup.py test" anymore. To run them manually, do: $ cd webob $ $MYVENV/bin/python setup.py develop $ cd docs $ $MYVENV/bin/python doctests.py Blamelist --------- - webob.acceptparse (aconrad) - webob.byterange (ppaez) - webob.cachecontrol (whit) - webob.dec (rafrombrc) - webob.descriptors (reedobrien) - webob.etag (shentonfreude) - webob.multidict (joe) - webob.request (tseaver) - webob.response (caseman/mcdonc) - webob.exc (joe) Doctest-to-Unit Test Conversion ------------------------------- - tests/test_request.txt (aconrad) - tests/test_response.txt (groner) WebOb-1.3.1/docs/pycon2011/request_table.rst0000644000175000017500000001370211636566135021302 0ustar chrismchrism00000000000000========================== Request Comparison Table ========================== b=WebBob z=Werkzeug x=both WEBOB NAME write read WERKZEUG NAME NOTES ================================= ===== ==== ================================= =========================================== Read-Write Properties Read-Write Properties +++++++++++++++++++++ +++++++++++++++++++++ content_type content_type CommonRequestDescriptorMixin charset charset "utf-8" headers headers cached_property urlvars urlargs host host cached_property body unicode_errors 'strict' encoding_errors 'ignore' decode_param_names F request_body_tempfile_limit 10*1024 max_content_length None Not sure if these are the same is_behind_proxy F max_form_memory_size None parameter_storage_class ImmutableMultiDict list_storage_class ImmutableList dict_storage_class ImmutableTypeConversionDict environ environ populate_request T shallow F Environ Getter Properties +++++++++++++++++++++++++ body_file_raw scheme method method http_version script_name script_root cached_property path_info ???path cached_property content_length content_type CommonRequestDescriptorMixin remote_user remote_user remote_addr remote_addr query_string query_string server_name host (with port) server_port host (with name) uscript_name upath_info is_body_seekable authorization authorization cached_property pragma pragma cached_property date date CommonRequestDescriptorMixin max_forwards max_forwards CommonRequestDescriptorMixin range if_range referer/referrer referrer CommonRequestDescriptorMixin user_agent user_agent cached_property input_stream mimetype CommonRequestDescriptorMixin Read-Only Properties ++++++++++++++++++++ host_url host_url cached_property application_url base_url cached_property Not sure if same path_url ???path cached_property path ???path cached_property path_qs ???path cached_property url url cached_property is_xhr is_xhr str_POST POST str_GET GET str_params params str_cookies cookies cookies cached_property url_charset stream cached_property args cached_property Maybe maps to params data cached_property form cached_property values cached_property Maybe maps to params files cached_property url_root cached_property access_route cached_property is_secure is_multithread is_multiprocess is_run_once Accept Properties +++++++++++++++++ accept accept_mimetypes accept_charset accept_charsets accept_encoding accept_encodings accept_language accept_languages Etag Properties +++++++++++++++ cache_control cache_control cached_property if_match if_match cached_property if_none_match if_none_match cached_property if_modified_since if_modified_since cached_property if_unmodified_since if_unmodified_since cached_property Methods ++++++ relative_url path_info_pop path_info_peek copy copy_get make_body_seekable copy_body make_tempfile remove_conditional_headers as_string (__str__) call_application get_response Classmethods ++++++++++++ from_string (classmethod) from_file blank from_values application Notes ----- mcdonc: script_root and path in werkzeug are not quite script_name and path_info in webob [17:51] the behavior regarding slashes is different for easier url joining WebOb-1.3.1/docs/pycon2011/response_table.rst0000644000175000017500000000730311743245714021444 0ustar chrismchrism00000000000000=========================== Response Comparison Table =========================== b=WebBob z=Werkzeug x=both =neither WEBOB NAME write read WERKZEUG NAME NOTES ================================= ===== ==== ================================= =========================================== default_content_type x x default_mimetype wb default: "text/html", wz: "text/plain" default_charset b b wz uses class var default for charset charset x x charset unicode_errors b b default_conditional_response b b from_file() (classmethod) b b copy b b status (string) x x status status_code x x status_code z default_status headers b b body b b unicode_body x x data body_file b File-like obj returned is writeable app_iter b x get_app_iter() z iter_encoded() allow b x allow vary b x vary content_type x x content_type content_type_params x x mime_type_params z z mime_type content_type str wo parameters content_length x x content_length content_encoding x x content_encoding content_language b x content_language content_location x x content_location content_md5 x x content_md5 content_disposition b b accept_ranges b b content_range b b date x x date expires x x expires last_modified x x last_modified cache_control b z cache_control cache_expires (dwim) b b conditional_response (bool) b x make_conditional() etag b x add_etag() etag b x get_etag() etag b x set_etag() z freeze() location x x location pragma b b age x x age retry_after x x retry_after server b b www_authenticate b z www_authenticate x x date retry_after x x retry_after set_cookie() set_cookie() delete_cookie() delete_cookie() unset_cookie() z is_streamed z is_sequence body_file x stream close() get_wsgi_headers() get_wsgi_response() __call__() __call__() WebOb-1.3.1/docs/test-file.txt0000644000175000017500000000004311635430213016666 0ustar chrismchrism00000000000000This is a test. Hello test people!WebOb-1.3.1/docs/test_dec.txt0000644000175000017500000000610211636566135016604 0ustar chrismchrism00000000000000A test of the decorator module:: >>> from doctest import ELLIPSIS >>> from webob.dec import wsgify >>> from webob import Response, Request >>> from webob import exc >>> @wsgify ... def test_app(req): ... return 'hey, this is a test: %s' % req.url >>> def testit(app, req): ... if isinstance(req, basestring): ... req = Request.blank(req) ... resp = req.get_response(app) ... print resp >>> testit(test_app, '/a url') 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 45 hey, this is a test: http://localhost/a%20url >>> test_app wsgify(test_app) Now some middleware testing:: >>> @wsgify.middleware ... def set_urlvar(req, app, **vars): ... req.urlvars.update(vars) ... return app(req) >>> @wsgify ... def show_vars(req): ... return 'These are the vars: %r' % (sorted(req.urlvars.items())) >>> show_vars2 = set_urlvar(show_vars, a=1, b=2) >>> show_vars2 wsgify.middleware(set_urlvar)(wsgify(show_vars), a=1, b=2) >>> testit(show_vars2, '/path') 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 40 These are the vars: [('a', 1), ('b', 2)] Some examples from Sergey:: >>> class HostMap(dict): ... @wsgify ... def __call__(self, req): ... return self[req.host.split(':')[0]] >>> app = HostMap() >>> app['example.com'] = Response('1') >>> app['other.com'] = Response('2') >>> print Request.blank('http://example.com/').get_response(wsgify(app)) 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 1 1 >>> @wsgify.middleware ... def override_https(req, normal_app, secure_app): ... if req.scheme == 'https': ... return secure_app ... else: ... return normal_app >>> app = override_https(Response('http'), secure_app=Response('https')) >>> print Request.blank('http://x.com/').get_response(app) 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 4 http A status checking middleware:: >>> @wsgify.middleware ... def catch(req, app, catchers): ... resp = req.get_response(app) ... return catchers.get(resp.status_int, resp) >>> @wsgify ... def simple(req): ... return other_app # Just to mess around >>> @wsgify ... def other_app(req): ... return Response('hey', status_int=int(req.path_info.strip('/'))) >>> app = catch(simple, catchers={500: Response('error!'), 404: Response('nothing')}) >>> print Request.blank('/200').get_response(app) 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 3 hey >>> print Request.blank('/500').get_response(app) 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 6 error! >>> print Request.blank('/404').get_response(app) 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 7 nothing WebOb-1.3.1/docs/conf.py0000644000175000017500000000166411645621753015556 0ustar chrismchrism00000000000000# -*- coding: utf-8 -*- from webob import __version__ extensions = ['sphinx.ext.autodoc'] source_suffix = '.txt' # The suffix of source filenames. master_doc = 'index' # The master toctree document. project = 'WebOb' copyright = '2011, Ian Bicking and contributors' version = release = __version__ exclude_patterns = ['jsonrpc-example-code/*'] modindex_common_prefix = ['webob.'] # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # html_favicon = ... html_add_permalinks = False #html_show_sourcelink = True # ?set to False? # Content template for the index page. #html_index = '' # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Output file base name for HTML help builder. htmlhelp_basename = 'WebObdoc' WebOb-1.3.1/docs/jsonrpc-example.txt0000644000175000017500000005135311743245714020125 0ustar chrismchrism00000000000000JSON-RPC Example ================ .. contents:: :author: Ian Bicking Introduction ------------ This is an example of how to write a web service using WebOb. The example shows how to create a `JSON-RPC `_ endpoint using WebOb and the `simplejson `_ JSON library. This also shows how to use WebOb as a client library using `WSGIProxy `_. While this example presents JSON-RPC, this is not an endorsement of JSON-RPC. In fact I don't like JSON-RPC. It's unnecessarily un-RESTful, and modelled too closely on `XML-RPC `_. Code ---- The finished code for this is available in `docs/json-example-code/jsonrpc.py `_ -- you can run that file as a script to try it out, or import it. Concepts -------- JSON-RPC wraps an object, allowing you to call methods on that object and get the return values. It also provides a way to get error responses. The `specification `_ goes into the details (though in a vague sort of way). Here's the basics: * All access goes through a POST to a single URL. * The POST contains a JSON body that looks like:: {"method": "methodName", "id": "arbitrary-something", "params": [arg1, arg2, ...]} * The ``id`` parameter is just a convenience for the client to keep track of which response goes with which request. This makes asynchronous calls (like an XMLHttpRequest) easier. We just send the exact same id back as we get, we never look at it. * The response is JSON. A successful response looks like:: {"result": the_result, "error": null, "id": "arbitrary-something"} * The error response looks like:: {"result": null, "error": {"name": "JSONRPCError", "code": (number 100-999), "message": "Some Error Occurred", "error": "whatever you want\n(a traceback?)"}, "id": "arbitrary-something"} * It doesn't seem to indicate if an error response should have a 200 response or a 500 response. So as not to be completely stupid about HTTP, we choose a 500 resonse, as giving an error with a 200 response is irresponsible. Infrastructure -------------- To make this easier to test, we'll set up a bit of infrastructure. This will open up a server (using `wsgiref `_) and serve up our application (note that *creating* the application is left out to start with): .. code-block:: python import sys def main(args=None): import optparse from wsgiref import simple_server parser = optparse.OptionParser( usage="%prog [OPTIONS] MODULE:EXPRESSION") parser.add_option( '-p', '--port', default='8080', help='Port to serve on (default 8080)') parser.add_option( '-H', '--host', default='127.0.0.1', help='Host to serve on (default localhost; 0.0.0.0 to make public)') if args is None: args = sys.argv[1:] options, args = parser.parse_args() if not args or len(args) > 1: print 'You must give a single object reference' parser.print_help() sys.exit(2) app = make_app(args[0]) server = simple_server.make_server( options.host, int(options.port), app) print 'Serving on http://%s:%s' % (options.host, options.port) server.serve_forever() if __name__ == '__main__': main() I won't describe this much. It starts a server, serving up just the app created by ``make_app(args[0])``. ``make_app`` will have to load up the object and wrap it in our WSGI/WebOb wrapper. We'll be calling that wrapper ``JSONRPC(obj)``, so here's how it'll go: .. code-block:: python def make_app(expr): module, expression = expr.split(':', 1) __import__(module) module = sys.modules[module] obj = eval(expression, module.__dict__) return JsonRpcApp(obj) We use ``__import__(module)`` to import the module, but its return value is wonky. We can find the thing it imported in ``sys.modules`` (a dictionary of all the loaded modules). Then we evaluate the second part of the expression in the namespace of the module. This lets you do something like ``smtplib:SMTP('localhost')`` to get a fully instantiated SMTP object. That's all the infrastructure we'll need for the server side. Now we just have to implement ``JsonRpcApp``. The Application Wrapper ----------------------- Note that I'm calling this an "application" because that's the terminology WSGI uses. Everything that gets *called* is an "application", and anything that calls an application is called a "server". The instantiation of the server is already figured out: .. code-block:: python class JsonRpcApp(object): def __init__(self, obj): self.obj = obj def __call__(self, environ, start_response): ... the WSGI interface ... So the server is an instance bound to the particular object being exposed, and ``__call__`` implements the WSGI interface. We'll start with a simple outline of the WSGI interface, using a kind of standard WebOb setup: .. code-block:: python from webob import Request, Response from webob import exc class JsonRpcApp(object): ... def __call__(self, environ, start_response): req = Request(environ) try: resp = self.process(req) except ValueError, e: resp = exc.HTTPBadRequest(str(e)) except exc.HTTPException, e: resp = e return resp(environ, start_response) We first create a request object. The request object just wraps the WSGI environment. Then we create the response object in the ``process`` method (which we still have to write). We also do some exception catching. We'll turn any ``ValueError`` into a ``400 Bad Request`` response. We'll also let ``process`` raise any ``web.exc.HTTPException`` exception. There's an exception defined in that module for all the HTTP error responses, like ``405 Method Not Allowed``. These exceptions are themselves WSGI applications (as is ``webob.Response``), and so we call them like WSGI applications and return the result. The ``process`` method ---------------------- The ``process`` method of course is where all the fancy stuff happens. We'll start with just the most minimal implementation, with no error checking or handling: .. code-block:: python from simplejson import loads, dumps class JsonRpcApp(object): ... def process(self, req): json = loads(req.body) method = json['method'] params = json['params'] id = json['id'] method = getattr(self.obj, method) result = method(*params) resp = Response( content_type='application/json', body=dumps(dict(result=result, error=None, id=id))) return resp As long as the request is properly formed and the method doesn't raise any exceptions, you are pretty much set. But of course that's not a reasonable expectation. There's a whole bunch of things that can go wrong. For instance, it has to be a POST method: .. code-block:: python if not req.method == 'POST': raise exc.HTTPMethodNotAllowed( "Only POST allowed", allowed='POST') And maybe the request body doesn't contain valid JSON: .. code-block:: python try: json = loads(req.body) except ValueError, e: raise ValueError('Bad JSON: %s' % e) And maybe all the keys aren't in the dictionary: .. code-block:: python try: method = json['method'] params = json['params'] id = json['id'] except KeyError, e: raise ValueError( "JSON body missing parameter: %s" % e) And maybe it's trying to acces a private method (a method that starts with ``_``) -- that's not just a bad request, we'll call that case ``403 Forbidden``. .. code-block:: python if method.startswith('_'): raise exc.HTTPForbidden( "Bad method name %s: must not start with _" % method) And maybe ``json['params']`` isn't a list: .. code-block:: python if not isinstance(params, list): raise ValueError( "Bad params %r: must be a list" % params) And maybe the method doesn't exist: .. code-block:: python try: method = getattr(self.obj, method) except AttributeError: raise ValueError( "No such method %s" % method) The last case is the error we actually can expect: that the method raises some exception. .. code-block:: python try: result = method(*params) except: tb = traceback.format_exc() exc_value = sys.exc_info()[1] error_value = dict( name='JSONRPCError', code=100, message=str(exc_value), error=tb) return Response( status=500, content_type='application/json', body=dumps(dict(result=None, error=error_value, id=id))) That's a complete server. The Complete Code ----------------- Since we showed all the error handling in pieces, here's the complete code: .. code-block:: python from webob import Request, Response from webob import exc from simplejson import loads, dumps import traceback import sys class JsonRpcApp(object): """ Serve the given object via json-rpc (http://json-rpc.org/) """ def __init__(self, obj): self.obj = obj def __call__(self, environ, start_response): req = Request(environ) try: resp = self.process(req) except ValueError, e: resp = exc.HTTPBadRequest(str(e)) except exc.HTTPException, e: resp = e return resp(environ, start_response) def process(self, req): if not req.method == 'POST': raise exc.HTTPMethodNotAllowed( "Only POST allowed", allowed='POST') try: json = loads(req.body) except ValueError, e: raise ValueError('Bad JSON: %s' % e) try: method = json['method'] params = json['params'] id = json['id'] except KeyError, e: raise ValueError( "JSON body missing parameter: %s" % e) if method.startswith('_'): raise exc.HTTPForbidden( "Bad method name %s: must not start with _" % method) if not isinstance(params, list): raise ValueError( "Bad params %r: must be a list" % params) try: method = getattr(self.obj, method) except AttributeError: raise ValueError( "No such method %s" % method) try: result = method(*params) except: text = traceback.format_exc() exc_value = sys.exc_info()[1] error_value = dict( name='JSONRPCError', code=100, message=str(exc_value), error=text) return Response( status=500, content_type='application/json', body=dumps(dict(result=None, error=error_value, id=id))) return Response( content_type='application/json', body=dumps(dict(result=result, error=None, id=id))) The Client ---------- It would be nice to have a client to test out our server. Using `WSGIProxy`_ we can use WebOb Request and Response to do actual HTTP connections. The basic idea is that you can create a blank Request: .. code-block:: python >>> from webob import Request >>> req = Request.blank('http://python.org') Then you can send that request to an application: .. code-block:: python >>> from wsgiproxy.exactproxy import proxy_exact_request >>> resp = req.get_response(proxy_exact_request) This particular application (``proxy_exact_request``) sends the request over HTTP: .. code-block:: python >>> resp.content_type 'text/html' >>> resp.body[:10] '`_ for our tests. The test is in `docs/json-example-code/test_jsonrpc.txt `_ and you can run it with `docs/json-example-code/test_jsonrpc.py `_, which looks like: .. code-block:: python if __name__ == '__main__': import doctest doctest.testfile('test_jsonrpc.txt') As you can see, it's just a stub to run the doctest. We'll need a simple object to expose. We'll make it real simple: .. code-block:: python >>> class Divider(object): ... def divide(self, a, b): ... return a / b Then we'll get the app setup: .. code-block:: python >>> from jsonrpc import * >>> app = JsonRpcApp(Divider()) And attach the client *directly* to it: .. code-block:: python >>> proxy = ServerProxy('http://localhost:8080', proxy=app) Because we gave the app itself as the proxy, the URL doesn't actually matter. Now, if you are used to testing you might ask: is this kosher? That is, we are shortcircuiting HTTP entirely. Is this a realistic test? One thing you might be worried about in this case is that there are more shared objects than you'd have with HTTP. That is, everything over HTTP is serialized to headers and bodies. Without HTTP, we can send stuff around that can't go over HTTP. This *could* happen, but we're mostly protected because the only thing the application's share is the WSGI ``environ``. Even though we use a ``webob.Request`` object on both side, it's not the *same* request object, and all the state is studiously kept in the environment. We *could* share things in the environment that couldn't go over HTTP. For instance, we could set ``environ['jsonrpc.request_value'] = dict(...)``, and avoid ``simplejson.dumps`` and ``simplejson.loads``. We *could* do that, and if we did then it is possible our test would work even though the libraries were broken over HTTP. But of course inspection shows we *don't* do that. A little discipline is required to resist playing clever tricks (or else you can play those tricks and do more testing). Generally it works well. So, now we have a proxy, lets use it: .. code-block:: python >>> proxy.divide(10, 4) 2 >>> proxy.divide(10, 4.0) 2.5 Lastly, we'll test a couple error conditions. First a method error: .. code-block:: python >>> proxy.divide(10, 0) # doctest: +ELLIPSIS Traceback (most recent call last): ... Fault: Method error calling http://localhost:8080: integer division or modulo by zero Traceback (most recent call last): File ... result = method(*params) File ... return a / b ZeroDivisionError: integer division or modulo by zero It's hard to actually predict this exception, because the test of the exception itself contains the traceback from the underlying call, with filenames and line numbers that aren't stable. We use ``# doctest: +ELLIPSIS`` so that we can replace text we don't care about with ``...``. This is actually figured out through copy-and-paste, and visual inspection to make sure it looks sensible. The other exception can be: .. code-block:: python >>> proxy.add(1, 1) Traceback (most recent call last): ... ProxyError: Error from JSON-RPC client http://localhost:8080: 400 Bad Request Here the exception isn't a JSON-RPC method exception, but a more basic ProxyError exception. Conclusion ---------- Hopefully this will give you ideas about how to implement web services of different kinds using WebOb. I hope you also can appreciate the elegance of the symmetry of the request and response objects, and the client and server for the protocol. Many of these techniques would be better used with a `RESTful `_ service, so do think about that direction if you are implementing your own protocol. WebOb-1.3.1/docs/wiki-example.txt0000664000175000017500000005746512201331167017412 0ustar chrismchrism00000000000000Wiki Example ============ :author: Ian Bicking .. contents:: Introduction ------------ This is an example of how to write a WSGI application using WebOb. WebOb isn't itself intended to write applications -- it is not a web framework on its own -- but it is *possible* to write applications using just WebOb. The `file serving example `_ is a better example of advanced HTTP usage. The `comment middleware example `_ is a better example of using middleware. This example provides some completeness by showing an application-focused end point. This example implements a very simple wiki. Code ---- The finished code for this is available in `docs/wiki-example-code/example.py `_ -- you can run that file as a script to try it out. Creating an Application ----------------------- A common pattern for creating small WSGI applications is to have a class which is instantiated with the configuration. For our application we'll be storing the pages under a directory. .. code-block:: python class WikiApp(object): def __init__(self, storage_dir): self.storage_dir = os.path.abspath(os.path.normpath(storage_dir)) WSGI applications are callables like ``wsgi_app(environ, start_response)``. *Instances* of `WikiApp` are WSGI applications, so we'll implement a ``__call__`` method: .. code-block:: python class WikiApp(object): ... def __call__(self, environ, start_response): # what we'll fill in To make the script runnable we'll create a simple command-line interface: .. code-block:: python if __name__ == '__main__': import optparse parser = optparse.OptionParser( usage='%prog --port=PORT' ) parser.add_option( '-p', '--port', default='8080', dest='port', type='int', help='Port to serve on (default 8080)') parser.add_option( '--wiki-data', default='./wiki', dest='wiki_data', help='Place to put wiki data into (default ./wiki/)') options, args = parser.parse_args() print 'Writing wiki pages to %s' % options.wiki_data app = WikiApp(options.wiki_data) from wsgiref.simple_server import make_server httpd = make_server('localhost', options.port, app) print 'Serving on http://localhost:%s' % options.port try: httpd.serve_forever() except KeyboardInterrupt: print '^C' There's not much to talk about in this code block. The application is instantiated and served with the built-in module `wsgiref.simple_server `_. The WSGI Application -------------------- Of course all the interesting stuff is in that ``__call__`` method. WebOb lets you ignore some of the details of WSGI, like what ``start_response`` really is. ``environ`` is a CGI-like dictionary, but ``webob.Request`` gives an object interface to it. ``webob.Response`` represents a response, and is itself a WSGI application. Here's kind of the hello world of WSGI applications using these objects: .. code-block:: python from webob import Request, Response class WikiApp(object): ... def __call__(self, environ, start_response): req = Request(environ) resp = Response( 'Hello %s!' % req.params.get('name', 'World')) return resp(environ, start_response) ``req.params.get('name', 'World')`` gets any query string parameter (like ``?name=Bob``), or if it's a POST form request it will look for a form parameter ``name``. We instantiate the response with the body of the response. You could also give keyword arguments like ``content_type='text/plain'`` (``text/html`` is the default content type and ``200 OK`` is the default status). For the wiki application we'll support a couple different kinds of screens, and we'll make our ``__call__`` method dispatch to different methods depending on the request. We'll support an ``action`` parameter like ``?action=edit``, and also dispatch on the method (GET, POST, etc, in ``req.method``). We'll pass in the request and expect a response object back. Also, WebOb has a series of exceptions in ``webob.exc``, like ``webob.exc.HTTPNotFound``, ``webob.exc.HTTPTemporaryRedirect``, etc. We'll also let the method raise one of these exceptions and turn it into a response. One last thing we'll do in our ``__call__`` method is create our ``Page`` object, which represents a wiki page. All this together makes: .. code-block:: python from webob import Request, Response from webob import exc class WikiApp(object): ... def __call__(self, environ, start_response): req = Request(environ) action = req.params.get('action', 'view') # Here's where we get the Page domain object: page = self.get_page(req.path_info) try: try: # The method name is action_{action_param}_{request_method}: meth = getattr(self, 'action_%s_%s' % (action, req.method)) except AttributeError: # If the method wasn't found there must be # something wrong with the request: raise exc.HTTPBadRequest('No such action %r' % action) resp = meth(req, page) except exc.HTTPException, e: # The exception object itself is a WSGI application/response: resp = e return resp(environ, start_response) The Domain Object ----------------- The ``Page`` domain object isn't really related to the web, but it is important to implementing this. Each ``Page`` is just a file on the filesystem. Our ``get_page`` method figures out the filename given the path (the path is in ``req.path_info``, which is all the path after the base path). The ``Page`` class handles getting and setting the title and content. Here's the method to figure out the filename: .. code-block:: python import os class WikiApp(object): ... def get_page(self, path): path = path.lstrip('/') if not path: # The path was '/', the home page path = 'index' path = os.path.join(self.storage_dir) path = os.path.normpath(path) if path.endswith('/'): path += 'index' if not path.startswith(self.storage_dir): raise exc.HTTPBadRequest("Bad path") path += '.html' return Page(path) Mostly this is just the kind of careful path construction you have to do when mapping a URL to a filename. While the server *may* normalize the path (so that a path like ``/../../`` can't be requested), you can never really be sure. By using ``os.path.normpath`` we eliminate these, and then we make absolutely sure that the resulting path is under our ``self.storage_dir`` with ``if not path.startswith(self.storage_dir): raise exc.HTTPBadRequest("Bad path")``. Here's the actual domain object: .. code-block:: python class Page(object): def __init__(self, filename): self.filename = filename @property def exists(self): return os.path.exists(self.filename) @property def title(self): if not self.exists: # we need to guess the title basename = os.path.splitext(os.path.basename(self.filename))[0] basename = re.sub(r'[_-]', ' ', basename) return basename.capitalize() content = self.full_content match = re.search(r'(.*?)', content, re.I|re.S) return match.group(1) @property def full_content(self): f = open(self.filename, 'rb') try: return f.read() finally: f.close() @property def content(self): if not self.exists: return '' content = self.full_content match = re.search(r']*>(.*?)', content, re.I|re.S) return match.group(1) @property def mtime(self): if not self.exists: return None else: return int(os.stat(self.filename).st_mtime) def set(self, title, content): dir = os.path.dirname(self.filename) if not os.path.exists(dir): os.makedirs(dir) new_content = """%s%s""" % ( title, content) f = open(self.filename, 'wb') f.write(new_content) f.close() Basically it provides a ``.title`` attribute, a ``.content`` attribute, the ``.mtime`` (last modified time), and the page can exist or not (giving appropriate guesses for title and content when the page does not exist). It encodes these on the filesystem as a simple HTML page that is parsed by some regular expressions. None of this really applies much to the web or WebOb, so I'll leave it to you to figure out the details of this. URLs, PATH_INFO, and SCRIPT_NAME -------------------------------- This is an aside for the tutorial, but an important concept. In WSGI, and accordingly with WebOb, the URL is split up into several pieces. Some of these are obvious and some not. An example:: http://example.com:8080/wiki/article/12?version=10 There are several components here: * req.scheme: ``http`` * req.host: ``example.com:8080`` * req.server_name: ``example.com`` * req.server_port: 8080 * req.script_name: ``/wiki`` * req.path_info: ``/article/12`` * req.query_string: ``version=10`` One non-obvious part is ``req.script_name`` and ``req.path_info``. These correspond to the CGI environmental variables ``SCRIPT_NAME`` and ``PATH_INFO``. ``req.script_name`` points to the *application*. You might have several applications in your site at different paths: one at ``/wiki``, one at ``/blog``, one at ``/``. Each application doesn't necessarily know about the others, but it has to construct its URLs properly -- so any internal links to the wiki application should start with ``/wiki``. Just as there are pieces to the URL, there are several properties in WebOb to construct URLs based on these: * req.host_url: ``http://example.com:8080`` * req.application_url: ``http://example.com:8080/wiki`` * req.path_url: ``http://example.com:8080/wiki/article/12`` * req.path: ``/wiki/article/12`` * req.path_qs: ``/wiki/article/12?version=10`` * req.url: ``http://example.com:8080/wiki/article/12?version10`` You can also create URLs with ``req.relative_url('some/other/page')``. In this example that would resolve to ``http://example.com:8080/wiki/article/some/other/page``. You can also create a relative URL to the application URL (SCRIPT_NAME) like ``req.relative_url('some/other/page', True)`` which would be ``http://example.com:8080/wiki/some/other/page``. Back to the Application ----------------------- We have a dispatching function with ``__call__`` and we have a domain object with ``Page``, but we aren't actually doing anything. The dispatching goes to ``action_ACTION_METHOD``, where ACTION defaults to ``view``. So a simple page view will be ``action_view_GET``. Let's implement that: .. code-block:: python class WikiApp(object): ... def action_view_GET(self, req, page): if not page.exists: return exc.HTTPTemporaryRedirect( location=req.url + '?action=edit') text = self.view_template.substitute( page=page, req=req) resp = Response(text) resp.last_modified = page.mtime resp.conditional_response = True return resp The first thing we do is redirect the user to the edit screen if the page doesn't exist. ``exc.HTTPTemporaryRedirect`` is a response that gives a ``307 Temporary Redirect`` response with the given location. Otherwise we fill in a template. The template language we're going to use in this example is `Tempita `_, a very simple template language with a similar interface to `string.Template `_. The template actually looks like this: .. code-block:: python from tempita import HTMLTemplate VIEW_TEMPLATE = HTMLTemplate("""\ {{page.title}}

{{page.title}}

{{page.content|html}}

Edit """) class WikiApp(object): view_template = VIEW_TEMPLATE ... As you can see it's a simple template using the title and the body, and a link to the edit screen. We copy the template object into a class method (``view_template = VIEW_TEMPLATE``) so that potentially a subclass could override these templates. ``tempita.HTMLTemplate`` is a template that does automatic HTML escaping. Our wiki will just be written in plain HTML, so we disable escaping of the content with ``{{page.content|html}}``. So let's look at the ``action_view_GET`` method again: .. code-block:: python def action_view_GET(self, req, page): if not page.exists: return exc.HTTPTemporaryRedirect( location=req.url + '?action=edit') text = self.view_template.substitute( page=page, req=req) resp = Response(text) resp.last_modified = page.mtime resp.conditional_response = True return resp The template should be pretty obvious now. We create a response with ``Response(text)``, which already has a default Content-Type of ``text/html``. To allow conditional responses we set ``resp.last_modified``. You can set this attribute to a date, None (effectively removing the header), a time tuple (like produced by ``time.localtime()``), or as in this case to an integer timestamp. If you get the value back it will always be a `datetime `_ object (or None). With this header we can process requests with If-Modified-Since headers, and return ``304 Not Modified`` if appropriate. It won't actually do that unless you set ``resp.conditional_response`` to True. .. note:: If you subclass ``webob.Response`` you can set the class attribute ``default_conditional_response = True`` and this setting will be on by default. You can also set other defaults, like the ``default_charset`` (``"utf8"``), or ``default_content_type`` (``"text/html"``). The Edit Screen --------------- The edit screen will be implemented in the method ``action_edit_GET``. There's a template and a very simple method: .. code-block:: python EDIT_TEMPLATE = HTMLTemplate("""\ Edit: {{page.title}} {{if page.exists}}

Edit: {{page.title}}

{{else}}

Create: {{page.title}}

{{endif}}
Title:
Content: Cancel

Cancel
""") class WikiApp(object): ... edit_template = EDIT_TEMPLATE def action_edit_GET(self, req, page): text = self.edit_template.substitute( page=page, req=req) return Response(text) As you can see, all the action here is in the template. In ``
`` we submit to ``req.path_url``; that's everything *but* ``?action=edit``. So we are POSTing right over the view page. This has the nice side effect of automatically invalidating any caches of the original page. It also is vaguely `RESTful `_. We save the last modified time in a hidden ``mtime`` field. This way we can detect concurrent updates. If start editing the page who's mtime is 100000, and someone else edits and saves a revision changing the mtime to 100010, we can use this hidden field to detect that conflict. Actually resolving the conflict is a little tricky and outside the scope of this particular tutorial, we'll just note the conflict to the user in an error. From there we just have a very straight-forward HTML form. Note that we don't quote the values because that is done automatically by ``HTMLTemplate``; if you are using something like ``string.Template`` or a templating language that doesn't do automatic quoting, you have to be careful to quote all the field values. We don't have any error conditions in our application, but if there were error conditions we might have to re-display this form with the input values the user already gave. In that case we'd do something like:: This way we use the value in the request (``req.params`` is both the query string parameters and any variables in a POST response), but if there is no value (e.g., first request) then we use the page values. Processing the Form ------------------- The form submits to ``action_view_POST`` (``view`` is the default action). So we have to implement that method: .. code-block:: python class WikiApp(object): ... def action_view_POST(self, req, page): submit_mtime = int(req.params.get('mtime') or '0') or None if page.mtime != submit_mtime: return exc.HTTPPreconditionFailed( "The page has been updated since you started editing it") page.set( title=req.params['title'], content=req.params['content']) resp = exc.HTTPSeeOther( location=req.path_url) return resp The first thing we do is check the mtime value. It can be an empty string (when there's no mtime, like when you are creating a page) or an integer. ``int(req.params.get('time') or '0') or None`` basically makes sure we don't pass ``""`` to ``int()`` (which is an error) then turns 0 into None (``0 or None`` will evaluate to None in Python -- ``false_value or other_value`` in Python resolves to ``other_value``). If it fails we just give a not-very-helpful error message, using ``412 Precondition Failed`` (typically preconditions are HTTP headers like ``If-Unmodified-Since``, but we can't really get the browser to send requests like that, so we use the hidden field instead). .. note:: Error statuses in HTTP are often under-used because people think they need to either return an error (useful for machines) or an error message or interface (useful for humans). In fact you can do both: you can give any human readable error message with your error response. One problem is that Internet Explorer will replace error messages with its own incredibly unhelpful error messages. However, it will only do this if the error message is short. If it's fairly large (4Kb is large enough) it will show the error message it was given. You can load your error with a big HTML comment to accomplish this, like ``"" % ('x'*4000)``. You can change the status of any response with ``resp.status_int = 412``, or you can change the body of an ``exc.HTTPSomething`` with ``resp.body = new_body``. The primary advantage of using the classes in ``webob.exc`` is giving the response a clear name and a boilerplate error message. After we check the mtime we get the form parameters from ``req.params`` and issue a redirect back to the original view page. ``303 See Other`` is a good response to give after accepting a POST form submission, as it gets rid of the POST (no warning messages for the user if they try to go back). In this example we've used ``req.params`` for all the form values. If we wanted to be specific about where we get the values from, they could come from ``req.GET`` (the query string, a misnomer since the query string is present even in POST requests) or ``req.POST`` (a POST form body). While sometimes it's nice to distinguish between these two locations, for the most part it doesn't matter. If you want to check the request method (e.g., make sure you can't change a page with a GET request) there's no reason to do it by accessing these method-specific getters. It's better to just handle the method specifically. We do it here by including the request method in our dispatcher (dispatching to ``action_view_GET`` or ``action_view_POST``). Cookies ------- One last little improvement we can do is show the user a message when they update the page, so it's not quite so mysteriously just another page view. A simple way to do this is to set a cookie after the save, then display it in the page view. To set it on save, we add a little to ``action_view_POST``: .. code-block:: python def action_view_POST(self, req, page): ... resp = exc.HTTPSeeOther( location=req.path_url) resp.set_cookie('message', 'Page updated') return resp And then in ``action_view_GET``: .. code-block:: python VIEW_TEMPLATE = HTMLTemplate("""\ ... {{if message}}
{{message}}
{{endif}} ...""") class WikiApp(object): ... def action_view_GET(self, req, page): ... if req.cookies.get('message'): message = req.cookies['message'] else: message = None text = self.view_template.substitute( page=page, req=req, message=message) resp = Response(text) if message: resp.delete_cookie('message') else: resp.last_modified = page.mtime resp.conditional_response = True return resp ``req.cookies`` is just a dictionary, and we also delete the cookie if it is present (so the message doesn't keep getting set). The conditional response stuff only applies when there isn't any message, as messages are private. Another alternative would be to display the message with Javascript, like:: Then put ```` in the page somewhere. This has the advantage of being very cacheable and simple on the server side. Conclusion ---------- We're done, hurrah! WebOb-1.3.1/docs/jsonrpc-example-code/0000775000175000017500000000000012252637046020266 5ustar chrismchrism00000000000000WebOb-1.3.1/docs/jsonrpc-example-code/test_jsonrpc.py0000644000175000017500000000012711635430213023342 0ustar chrismchrism00000000000000if __name__ == '__main__': import doctest doctest.testfile('test_jsonrpc.txt') WebOb-1.3.1/docs/jsonrpc-example-code/jsonrpc.py0000644000175000017500000001352411743245714022322 0ustar chrismchrism00000000000000# A reaction to: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/552751 from webob import Request, Response from webob import exc from simplejson import loads, dumps import traceback import sys class JsonRpcApp(object): """ Serve the given object via json-rpc (http://json-rpc.org/) """ def __init__(self, obj): self.obj = obj def __call__(self, environ, start_response): req = Request(environ) try: resp = self.process(req) except ValueError, e: resp = exc.HTTPBadRequest(str(e)) except exc.HTTPException, e: resp = e return resp(environ, start_response) def process(self, req): if not req.method == 'POST': raise exc.HTTPMethodNotAllowed( "Only POST allowed", allowed='POST') try: json = loads(req.body) except ValueError, e: raise ValueError('Bad JSON: %s' % e) try: method = json['method'] params = json['params'] id = json['id'] except KeyError, e: raise ValueError( "JSON body missing parameter: %s" % e) if method.startswith('_'): raise exc.HTTPForbidden( "Bad method name %s: must not start with _" % method) if not isinstance(params, list): raise ValueError( "Bad params %r: must be a list" % params) try: method = getattr(self.obj, method) except AttributeError: raise ValueError( "No such method %s" % method) try: result = method(*params) except: text = traceback.format_exc() exc_value = sys.exc_info()[1] error_value = dict( name='JSONRPCError', code=100, message=str(exc_value), error=text) return Response( status=500, content_type='application/json', body=dumps(dict(result=None, error=error_value, id=id))) return Response( content_type='application/json', body=dumps(dict(result=result, error=None, id=id))) class ServerProxy(object): """ JSON proxy to a remote service. """ def __init__(self, url, proxy=None): self._url = url if proxy is None: from wsgiproxy.exactproxy import proxy_exact_request proxy = proxy_exact_request self.proxy = proxy def __getattr__(self, name): if name.startswith('_'): raise AttributeError(name) return _Method(self, name) def __repr__(self): return '<%s for %s>' % ( self.__class__.__name__, self._url) class _Method(object): def __init__(self, parent, name): self.parent = parent self.name = name def __call__(self, *args): json = dict(method=self.name, id=None, params=list(args)) req = Request.blank(self.parent._url) req.method = 'POST' req.content_type = 'application/json' req.body = dumps(json) resp = req.get_response(self.parent.proxy) if resp.status_code != 200 and not ( resp.status_code == 500 and resp.content_type == 'application/json'): raise ProxyError( "Error from JSON-RPC client %s: %s" % (self.parent._url, resp.status), resp) json = loads(resp.body) if json.get('error') is not None: e = Fault( json['error'].get('message'), json['error'].get('code'), json['error'].get('error'), resp) raise e return json['result'] class ProxyError(Exception): """ Raised when a request via ServerProxy breaks """ def __init__(self, message, response): Exception.__init__(self, message) self.response = response class Fault(Exception): """ Raised when there is a remote error """ def __init__(self, message, code, error, response): Exception.__init__(self, message) self.code = code self.error = error self.response = response def __str__(self): return 'Method error calling %s: %s\n%s' % ( self.response.request.url, self.args[0], self.error) class DemoObject(object): """ Something interesting to attach to """ def add(self, *args): return sum(args) def average(self, *args): return sum(args) / float(len(args)) def divide(self, a, b): return a / b def make_app(expr): module, expression = expr.split(':', 1) __import__(module) module = sys.modules[module] obj = eval(expression, module.__dict__) return JsonRpcApp(obj) def main(args=None): import optparse from wsgiref import simple_server parser = optparse.OptionParser( usage='%prog [OPTIONS] MODULE:EXPRESSION') parser.add_option( '-p', '--port', default='8080', help='Port to serve on (default 8080)') parser.add_option( '-H', '--host', default='127.0.0.1', help='Host to serve on (default localhost; 0.0.0.0 to make public)') options, args = parser.parse_args() if not args or len(args) > 1: print 'You must give a single object reference' parser.print_help() sys.exit(2) app = make_app(args[0]) server = simple_server.make_server(options.host, int(options.port), app) print 'Serving on http://%s:%s' % (options.host, options.port) server.serve_forever() # Try python jsonrpc.py 'jsonrpc:DemoObject()' if __name__ == '__main__': main() WebOb-1.3.1/docs/jsonrpc-example-code/test_jsonrpc.txt0000644000175000017500000000156711635430213023542 0ustar chrismchrism00000000000000This is a test of the ``jsonrpc.py`` module:: >>> class Divider(object): ... def divide(self, a, b): ... return a / b >>> from jsonrpc import * >>> app = JsonRpcApp(Divider()) >>> proxy = ServerProxy('http://localhost:8080', proxy=app) >>> proxy.divide(10, 4) 2 >>> proxy.divide(10, 4.0) 2.5 >>> proxy.divide(10, 0) # doctest: +ELLIPSIS Traceback (most recent call last): ... Fault: Method error calling http://localhost:8080: integer division or modulo by zero Traceback (most recent call last): File ... result = method(*params) File ... return a / b ZeroDivisionError: integer division or modulo by zero >>> proxy.add(1, 1) Traceback (most recent call last): ... ProxyError: Error from JSON-RPC client http://localhost:8080: 400 Bad Request WebOb-1.3.1/docs/todo.txt0000664000175000017500000000033012044266064015746 0ustar chrismchrism00000000000000- header_getter - get rid of environ_decoder (push it into environ_getter) - Setting a value via request.headers['Foo'] = 'bar' will now raise an encoding error if the value is Unicode and not ascii-compatible. WebOb-1.3.1/docs/doctests.py0000644000175000017500000000126711636566135016462 0ustar chrismchrism00000000000000import unittest import doctest def test_suite(): flags = doctest.ELLIPSIS|doctest.NORMALIZE_WHITESPACE return unittest.TestSuite(( doctest.DocFileSuite('test_request.txt', optionflags=flags), doctest.DocFileSuite('test_response.txt', optionflags=flags), doctest.DocFileSuite('test_dec.txt', optionflags=flags), doctest.DocFileSuite('do-it-yourself.txt', optionflags=flags), doctest.DocFileSuite('file-example.txt', optionflags=flags), doctest.DocFileSuite('index.txt', optionflags=flags), doctest.DocFileSuite('reference.txt', optionflags=flags), )) if __name__ == '__main__': unittest.main(defaultTest='test_suite') WebOb-1.3.1/docs/comment-example.txt0000644000175000017500000003324011743245714020104 0ustar chrismchrism00000000000000Comment Example =============== .. contents:: Introduction ------------ This is an example of how to write WSGI middleware with WebOb. The specific example adds a simple comment form to HTML web pages; any page served through the middleware that is HTML gets a comment form added to it, and shows any existing comments. Code ---- The finished code for this is available in `docs/comment-example-code/example.py `_ -- you can run that file as a script to try it out. Instantiating Middleware ------------------------ Middleware of any complexity at all is usually best created as a class with its configuration as arguments to that class. Every middleware needs an application (``app``) that it wraps. This middleware also needs a location to store the comments; we'll put them all in a single directory. .. code-block:: python import os class Commenter(object): def __init__(self, app, storage_dir): self.app = app self.storage_dir = storage_dir if not os.path.exists(storage_dir): os.makedirs(storage_dir) When you use this middleware, you'll use it like: .. code-block:: python app = ... make the application ... app = Commenter(app, storage_dir='./comments') For our application we'll use a simple static file server that is included with `Paste `_ (use ``easy_install Paste`` to install this). The setup is all at the bottom of ``example.py``, and looks like this: .. code-block:: python if __name__ == '__main__': import optparse parser = optparse.OptionParser( usage='%prog --port=PORT BASE_DIRECTORY' ) parser.add_option( '-p', '--port', default='8080', dest='port', type='int', help='Port to serve on (default 8080)') parser.add_option( '--comment-data', default='./comments', dest='comment_data', help='Place to put comment data into (default ./comments/)') options, args = parser.parse_args() if not args: parser.error('You must give a BASE_DIRECTORY') base_dir = args[0] from paste.urlparser import StaticURLParser app = StaticURLParser(base_dir) app = Commenter(app, options.comment_data) from wsgiref.simple_server import make_server httpd = make_server('localhost', options.port, app) print 'Serving on http://localhost:%s' % options.port try: httpd.serve_forever() except KeyboardInterrupt: print '^C' I won't explain it here, but basically it takes some options, creates an application that serves static files (``StaticURLParser(base_dir)``), wraps it with ``Commenter(app, options.comment_data)`` then serves that. The Middleware -------------- While we've created the class structure for the middleware, it doesn't actually do anything. Here's a kind of minimal version of the middleware (using WebOb): .. code-block:: python from webob import Request class Commenter(object): def __init__(self, app, storage_dir): self.app = app self.storage_dir = storage_dir if not os.path.exists(storage_dir): os.makedirs(storage_dir) def __call__(self, environ, start_response): req = Request(environ) resp = req.get_response(self.app) return resp(environ, start_response) This doesn't modify the response it any way. You could write it like this without WebOb: .. code-block:: python class Commenter(object): ... def __call__(self, environ, start_response): return self.app(environ, start_response) But it won't be as convenient later. First, lets create a little bit of infrastructure for our middleware. We need to save and load per-url data (the comments themselves). We'll keep them in pickles, where each url has a pickle named after the url (but double-quoted, so ``http://localhost:8080/index.html`` becomes ``http%3A%2F%2Flocalhost%3A8080%2Findex.html``). .. code-block:: python from cPickle import load, dump class Commenter(object): ... def get_data(self, url): filename = self.url_filename(url) if not os.path.exists(filename): return [] else: f = open(filename, 'rb') data = load(f) f.close() return data def save_data(self, url, data): filename = self.url_filename(url) f = open(filename, 'wb') dump(data, f) f.close() def url_filename(self, url): # Double-quoting makes the filename safe return os.path.join(self.storage_dir, urllib.quote(url, '')) You can get the full request URL with ``req.url``, so to get the comment data with these methods you do ``data = self.get_data(req.url)``. Now we'll update the ``__call__`` method to filter *some* responses, and get the comment data for those. We don't want to change responses that were error responses (anything but ``200``), nor do we want to filter responses that aren't HTML. So we get: .. code-block:: python class Commenter(object): ... def __call__(self, environ, start_response): req = Request(environ) resp = req.get_response(self.app) if resp.content_type != 'text/html' or resp.status_code != 200: return resp(environ, start_response) data = self.get_data(req.url) ... do stuff with data, update resp ... return resp(environ, start_response) So far we're punting on actually adding the comments to the page. We also haven't defined what ``data`` will hold. Let's say it's a list of dictionaries, where each dictionary looks like ``{'name': 'John Doe', 'homepage': 'http://blog.johndoe.com', 'comments': 'Great site!'}``. We'll also need a simple method to add stuff to the page. We'll use a regular expression to find the end of the page and put text in: .. code-block:: python import re class Commenter(object): ... _end_body_re = re.compile(r'', re.I|re.S) def add_to_end(self, html, extra_html): """ Adds extra_html to the end of the html page (before ) """ match = self._end_body_re.search(html) if not match: return html + extra_html else: return html[:match.start()] + extra_html + html[match.start():] And then we'll use it like: .. code-block:: python data = self.get_data(req.url) body = resp.body body = self.add_to_end(body, self.format_comments(data)) resp.body = body return resp(environ, start_response) We get the body, update it, and put it back in the response. This also updates ``Content-Length``. Then we define: .. code-block:: python from webob import html_escape class Commenter(object): ... def format_comments(self, comments): if not comments: return '' text = [] text.append('
') text.append('

Comments (%s):

' % len(comments)) for comment in comments: text.append('

%s at %s:

' % ( html_escape(comment['homepage']), html_escape(comment['name']), time.strftime('%c', comment['time']))) # Susceptible to XSS attacks!: text.append(comment['comments']) return ''.join(text) We put in a header (with an anchor we'll use later), and a section for each comment. Note that ``html_escape`` is the same as ``cgi.escape`` and just turns ``&`` into ``&``, etc. Because we put in some text without quoting it is susceptible to a `Cross-Site Scripting `_ attack. Fixing that is beyond the scope of this tutorial; you could quote it or clean it with something like `lxml.html.clean `_. Accepting Comments ------------------ All of those pieces *display* comments, but still no one can actually make comments. To handle this we'll take a little piece of the URL space for our own, everything under ``/.comments``, so when someone POSTs there it will add a comment. When the request comes in there are two parts to the path: ``SCRIPT_NAME`` and ``PATH_INFO``. Everything in ``SCRIPT_NAME`` has already been parsed, and everything in ``PATH_INFO`` has yet to be parsed. That means that the URL *without* ``PATH_INFO`` is the path to the middleware; we can intercept anything else below ``SCRIPT_NAME`` but nothing above it. The name for the URL without ``PATH_INFO`` is ``req.application_url``. We have to capture it early to make sure it doesn't change (since the WSGI application we are wrapping may update ``SCRIPT_NAME`` and ``PATH_INFO``). So here's what this all looks like: .. code-block:: python class Commenter(object): ... def __call__(self, environ, start_response): req = Request(environ) if req.path_info_peek() == '.comments': return self.process_comment(req)(environ, start_response) # This is the base path of *this* middleware: base_url = req.application_url resp = req.get_response(self.app) if resp.content_type != 'text/html' or resp.status_code != 200: # Not an HTML response, we don't want to # do anything to it return resp(environ, start_response) # Make sure the content isn't gzipped: resp.decode_content() comments = self.get_data(req.url) body = resp.body body = self.add_to_end(body, self.format_comments(comments)) body = self.add_to_end(body, self.submit_form(base_url, req)) resp.body = body return resp(environ, start_response) ``base_url`` is the path where the middleware is located (if you run the example server, it will be ``http://localhost:PORT/``). We use ``req.path_info_peek()`` to look at the next segment of the URL -- what comes after base_url. If it is ``.comments`` then we handle it internally and don't pass the request on. We also put in a little guard, ``resp.decode_content()`` in case the application returns a gzipped response. Then we get the data, add the comments, add the *form* to make new comments, and return the result. submit_form ~~~~~~~~~~~ Here's what the form looks like: .. code-block:: python class Commenter(object): ... def submit_form(self, base_path, req): return '''

Leave a comment:

Name:
URL:
Comments:

''' % (base_path, html_escape(req.url)) Nothing too exciting. It submits a form with the keys ``url`` (the URL being commented on), ``name``, ``homepage``, and ``comments``. process_comment ~~~~~~~~~~~~~~~ If you look at the method call, what we do is call the method then treat the result as a WSGI application: .. code-block:: python return self.process_comment(req)(environ, start_response) You could write this as: .. code-block:: python response = self.process_comment(req) return response(environ, start_response) A common pattern in WSGI middleware that *doesn't* use WebOb is to just do: .. code-block:: python return self.process_comment(environ, start_response) But the WebOb style makes it easier to modify the response if you want to; modifying a traditional WSGI response/application output requires changing your logic flow considerably. Here's the actual processing code: .. code-block:: python from webob import exc from webob import Response class Commenter(object): ... def process_comment(self, req): try: url = req.params['url'] name = req.params['name'] homepage = req.params['homepage'] comments = req.params['comments'] except KeyError, e: resp = exc.HTTPBadRequest('Missing parameter: %s' % e) return resp data = self.get_data(url) data.append(dict( name=name, homepage=homepage, comments=comments, time=time.gmtime())) self.save_data(url, data) resp = exc.HTTPSeeOther(location=url+'#comment-area') return resp We either give a Bad Request response (if the form submission is somehow malformed), or a redirect back to the original page. The classes in ``webob.exc`` (like ``HTTPBadRequest`` and ``HTTPSeeOther``) are Response subclasses that can be used to quickly create responses for these non-200 cases where the response body usually doesn't matter much. Conclusion ---------- This shows how to make response modifying middleware, which is probably the most difficult kind of middleware to write with WSGI -- modifying the request is quite simple in comparison, as you simply update ``environ``. WebOb-1.3.1/docs/do-it-yourself.txt0000644000175000017500000006464111636566135017710 0ustar chrismchrism00000000000000Another Do-It-Yourself Framework ================================ .. contents:: Introduction and Audience ------------------------- It's been over two years since I wrote the `first version of this tutorial `_. I decided to give it another run with some of the tools that have come about since then (particularly `WebOb `_). Sometimes Python is accused of having too many web frameworks. And it's true, there are a lot. That said, I think writing a framework is a useful exercise. It doesn't let you skip over too much without understanding it. It removes the magic. So even if you go on to use another existing framework (which I'd probably advise you do), you'll be able to understand it better if you've written something like it on your own. This tutorial shows you how to create a web framework of your own, using WSGI and WebOb. No other libraries will be used. For the longer sections I will try to explain any tricky parts on a line-by line basis following the example. What Is WSGI? ------------- At its simplest WSGI is an interface between web servers and web applications. We'll explain the mechanics of WSGI below, but a higher level view is to say that WSGI lets code pass around web requests in a fairly formal way. That's the simplest summary, but there is more -- WSGI lets you add annotation to the request, and adds some more metadata to the request. WSGI more specifically is made up of an *application* and a *server*. The application is a function that receives the request and produces the response. The server is the thing that calls the application function. A very simple application looks like this: .. code-block:: python >>> def application(environ, start_response): ... start_response('200 OK', [('Content-Type', 'text/html')]) ... return ['Hello World!'] The ``environ`` argument is a dictionary with values like the environment in a CGI request. The header ``Host:``, for instance, goes in ``environ['HTTP_HOST']``. The path is in ``environ['SCRIPT_NAME']`` (which is the path leading *up to* the application), and ``environ['PATH_INFO']`` (the remaining path that the application should interpret). We won't focus much on the server, but we will use WebOb to handle the application. WebOb in a way has a simple server interface. To use it you create a new request with ``req = webob.Request.blank('http://localhost/test')``, and then call the application with ``resp = req.get_response(app)``. For example: .. code-block:: python >>> from webob import Request >>> req = Request.blank('http://localhost/test') >>> resp = req.get_response(application) >>> print resp 200 OK Content-Type: text/html Hello World! This is an easy way to test applications, and we'll use it to test the framework we're creating. About WebOb ----------- WebOb is a library to create a request and response object. It's centered around the WSGI model. Requests are wrappers around the environment. For example: .. code-block:: python >>> req = Request.blank('http://localhost/test') >>> req.environ['HTTP_HOST'] 'localhost:80' >>> req.host 'localhost:80' >>> req.path_info '/test' Responses are objects that represent the... well, response. The status, headers, and body: .. code-block:: python >>> from webob import Response >>> resp = Response(body='Hello World!') >>> resp.content_type 'text/html' >>> resp.content_type = 'text/plain' >>> print resp 200 OK Content-Length: 12 Content-Type: text/plain; charset=UTF-8 Hello World! Responses also happen to be WSGI applications. That means you can call ``resp(environ, start_response)``. Of course it's much less *dynamic* than a normal WSGI application. These two pieces solve a lot of the more tedious parts of making a framework. They deal with parsing most HTTP headers, generating valid responses, and a number of unicode issues. Serving Your Application ------------------------ While we can test the application using WebOb, you might want to serve the application. Here's the basic recipe, using the `Paste `_ HTTP server: .. code-block:: python if __name__ == '__main__': from paste import httpserver httpserver.serve(app, host='127.0.0.1', port=8080) you could also use `wsgiref `_ from the standard library, but this is mostly appropriate for testing as it is single-threaded: .. code-block:: python if __name__ == '__main__': from wsgiref.simple_server import make_server server = make_server('127.0.0.1', 8080, app) server.serve_forever() Making A Framework ------------------ Well, now we need to start work on our framework. Here's the basic model we'll be creating: * We'll define routes that point to controllers * We'll create a simple framework for creating controllers Routing ------- We'll use explicit routes using URI templates (minus the domains) to match paths. We'll add a little extension that you can use ``{name:regular expression}``, where the named segment must then match that regular expression. The matches will include a "controller" variable, which will be a string like "module_name:function_name". For our examples we'll use a simple blog. So here's what a route would look like: .. code-block:: python app = Router() app.add_route('/', controller='controllers:index') app.add_route('/{year:\d\d\d\d}/', controller='controllers:archive') app.add_route('/{year:\d\d\d\d}/{month:\d\d}/', controller='controllers:archive') app.add_route('/{year:\d\d\d\d}/{month:\d\d}/{slug}', controller='controllers:view') app.add_route('/post', controller='controllers:post') To do this we'll need a couple pieces: * Something to match those URI template things. * Something to load the controller * The object to patch them together (``Router``) Routing: Templates ~~~~~~~~~~~~~~~~~~ To do the matching, we'll compile those templates to regular expressions. .. code-block:: python :linenos: >>> import re >>> var_regex = re.compile(r''' ... \{ # The exact character "{" ... (\w+) # The variable name (restricted to a-z, 0-9, _) ... (?::([^}]+))? # The optional :regex part ... \} # The exact character "}" ... ''', re.VERBOSE) >>> def template_to_regex(template): ... regex = '' ... last_pos = 0 ... for match in var_regex.finditer(template): ... regex += re.escape(template[last_pos:match.start()]) ... var_name = match.group(1) ... expr = match.group(2) or '[^/]+' ... expr = '(?P<%s>%s)' % (var_name, expr) ... regex += expr ... last_pos = match.end() ... regex += re.escape(template[last_pos:]) ... regex = '^%s$' % regex ... return regex **line 2:** Here we create the regular expression. The ``re.VERBOSE`` flag makes the regular expression parser ignore whitespace and allow comments, so we can avoid some of the feel of line-noise. This matches any variables, i.e., ``{var:regex}`` (where ``:regex`` is optional). Note that there are two groups we capture: ``match.group(1)`` will be the variable name, and ``match.group(2)`` will be the regular expression (or None when there is no regular expression). Note that ``(?:...)?`` means that the section is optional. **line 10**: This variable will hold the regular expression that we are creating. **line 11**: This contains the position of the end of the last match. **line 12**: The ``finditer`` method yields all the matches. **line 13**: We're getting all the non-``{}`` text from after the last match, up to the beginning of this match. We call ``re.escape`` on that text, which escapes any characters that have special meaning. So ``.html`` will be escaped as ``\.html``. **line 14**: The first match is the variable name. **line 15**: ``expr`` is the regular expression we'll match against, the optional second match. The default is ``[^/]+``, which matches any non-empty, non-/ string. Which seems like a reasonable default to me. **line 16**: Here we create the actual regular expression. ``(?P...)`` is a grouped expression that is named. When you get a match, you can look at ``match.groupdict()`` and get the names and values. **line 17, 18**: We add the expression on to the complete regular expression and save the last position. **line 19**: We add remaining non-variable text to the regular expression. **line 20**: And then we make the regular expression match the complete string (``^`` to force it to match from the start, ``$`` to make sure it matches up to the end). To test it we can try some translations. You could put these directly in the docstring of the ``template_to_regex`` function and use `doctest `_ to test that. But I'm using doctest to test *this* document, so I can't put a docstring doctest inside the doctest itself. Anyway, here's what a test looks like: .. code-block:: python >>> print template_to_regex('/a/static/path') ^\/a\/static\/path$ >>> print template_to_regex('/{year:\d\d\d\d}/{month:\d\d}/{slug}') ^\/(?P\d\d\d\d)\/(?P\d\d)\/(?P[^/]+)$ Routing: controller loading ~~~~~~~~~~~~~~~~~~~~~~~~~~~ To load controllers we have to import the module, then get the function out of it. We'll use the ``__import__`` builtin to import the module. The return value of ``__import__`` isn't very useful, but it puts the module into ``sys.modules``, a dictionary of all the loaded modules. Also, some people don't know how exactly the string method ``split`` works. It takes two arguments -- the first is the character to split on, and the second is the maximum number of splits to do. We want to split on just the first ``:`` character, so we'll use a maximum number of splits of 1. .. code-block:: python >>> import sys >>> def load_controller(string): ... module_name, func_name = string.split(':', 1) ... __import__(module_name) ... module = sys.modules[module_name] ... func = getattr(module, func_name) ... return func Routing: putting it together ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Now, the ``Router`` class. The class has the ``add_route`` method, and also a ``__call__`` method. That ``__call__`` method makes the Router object itself a WSGI application. So when a request comes in, it looks at ``PATH_INFO`` (also known as ``req.path_info``) and hands off the request to the controller that matches that path. .. code-block:: python :linenos: >>> from webob import Request >>> from webob import exc >>> class Router(object): ... def __init__(self): ... self.routes = [] ... ... def add_route(self, template, controller, **vars): ... if isinstance(controller, basestring): ... controller = load_controller(controller) ... self.routes.append((re.compile(template_to_regex(template)), ... controller, ... vars)) ... ... def __call__(self, environ, start_response): ... req = Request(environ) ... for regex, controller, vars in self.routes: ... match = regex.match(req.path_info) ... if match: ... req.urlvars = match.groupdict() ... req.urlvars.update(vars) ... return controller(environ, start_response) ... return exc.HTTPNotFound()(environ, start_response) **line 5**: We are going to keep the route options in an ordered list. Each item will be ``(regex, controller, vars)``: ``regex`` is the regular expression object to match against, ``controller`` is the controller to run, and ``vars`` are any extra (constant) variables. **line 8, 9**: We will allow you to call ``add_route`` with a string (that will be imported) or a controller object. We test for a string here, and then import it if necessary. **line 13**: Here we add a ``__call__`` method. This is the method used when you call an object like a function. You should recognize this as the WSGI signature. **line 14**: We create a request object. Note we'll only use this request object in this function; if the controller wants a request object it'll have to make on of its own. **line 16**: We test the regular expression against ``req.path_info``. This is the same as ``environ['PATH_INFO']``. That's all the request path left to be processed. **line 18**: We set ``req.urlvars`` to the dictionary of matches in the regular expression. This variable actually maps to ``environ['wsgiorg.routing_args']``. Any attributes you set on a request will, in one way or another, map to the environment dictionary: the request holds no state of its own. **line 19**: We also add in any explicit variables passed in through ``add_route()``. **line 20**: Then we call the controller as a WSGI application itself. Any fancy framework stuff the controller wants to do, it'll have to do itself. **line 21**: If nothing matches, we return a 404 Not Found response. ``webob.exc.HTTPNotFound()`` is a WSGI application that returns 404 responses. You could add a message too, like ``webob.exc.HTTPNotFound('No route matched')``. Then, of course, we call the application. Controllers ----------- The router just passes the request on to the controller, so the controllers are themselves just WSGI applications. But we'll want to set up something to make those applications friendlier to write. To do that we'll write a `decorator `_. A decorator is a function that wraps another function. After decoration the function will be a WSGI application, but it will be decorating a function with a signature like ``controller_func(req, **urlvars)``. The controller function will return a response object (which, remember, is a WSGI application on its own). .. code-block:: python :linenos: >>> from webob import Request, Response >>> from webob import exc >>> def controller(func): ... def replacement(environ, start_response): ... req = Request(environ) ... try: ... resp = func(req, **req.urlvars) ... except exc.HTTPException, e: ... resp = e ... if isinstance(resp, basestring): ... resp = Response(body=resp) ... return resp(environ, start_response) ... return replacement **line 3**: This is the typical signature for a decorator -- it takes one function as an argument, and returns a wrapped function. **line 4**: This is the replacement function we'll return. This is called a `closure `_ -- this function will have access to ``func``, and everytime you decorate a new function there will be a new ``replacement`` function with its own value of ``func``. As you can see, this is a WSGI application. **line 5**: We create a request. **line 6**: Here we catch any ``webob.exc.HTTPException`` exceptions. This is so you can do ``raise webob.exc.HTTPNotFound()`` in your function. These exceptions are themselves WSGI applications. **line 7**: We call the function with the request object, any any variables in ``req.urlvars``. And we get back a response. **line 10**: We'll allow the function to return a full response object, or just a string. If they return a string, we'll create a ``Response`` object with that (and with the standard ``200 OK`` status, ``text/html`` content type, and ``utf8`` charset/encoding). **line 12**: We pass the request on to the response. Which *also* happens to be a WSGI application. WSGI applications are falling from the sky! **line 13**: We return the function object itself, which will take the place of the function. You use this controller like: .. code-block:: python >>> @controller ... def index(req): ... return 'This is the index' Putting It Together ------------------- Now we'll show a basic application. Just a hello world application for now. Note that this document is the module ``__main__``. .. code-block:: python >>> @controller ... def hello(req): ... if req.method == 'POST': ... return 'Hello %s!' % req.params['name'] ... elif req.method == 'GET': ... return '''
... You're name: ... ...
''' >>> hello_world = Router() >>> hello_world.add_route('/', controller=hello) Now let's test that application: .. code-block:: python >>> req = Request.blank('/') >>> resp = req.get_response(hello_world) >>> print resp 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 131
You're name:
>>> req.method = 'POST' >>> req.body = 'name=Ian' >>> resp = req.get_response(hello_world) >>> print resp 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 10 Hello Ian! Another Controller ------------------ There's another pattern that might be interesting to try for a controller. Instead of a function, we can make a class with methods like ``get``, ``post``, etc. The ``urlvars`` will be used to instantiate the class. We could do this as a superclass, but the implementation will be more elegant as a wrapper, like the decorator is a wrapper. Python 3.0 will add `class decorators `_ which will work like this. We'll allow an extra ``action`` variable, which will define the method (actually ``action_method``, where ``_method`` is the request method). If no action is given, we'll use just the method (i.e., ``get``, ``post``, etc). .. code-block:: python :linenos: >>> def rest_controller(cls): ... def replacement(environ, start_response): ... req = Request(environ) ... try: ... instance = cls(req, **req.urlvars) ... action = req.urlvars.get('action') ... if action: ... action += '_' + req.method.lower() ... else: ... action = req.method.lower() ... try: ... method = getattr(instance, action) ... except AttributeError: ... raise exc.HTTPNotFound("No action %s" % action) ... resp = method() ... if isinstance(resp, basestring): ... resp = Response(body=resp) ... except exc.HTTPException, e: ... resp = e ... return resp(environ, start_response) ... return replacement **line 1**: Here we're kind of decorating a class. But really we'll just create a WSGI application wrapper. **line 2-4**: The replacement WSGI application, also a closure. And we create a request and catch exceptions, just like in the decorator. **line 5**: We instantiate the class with both the request and ``req.urlvars`` to initialize it. The instance will only be used for one request. (Note that the *instance* then doesn't have to be thread safe.) **line 6**: We get the action variable out, if there is one. **line 7, 8**: If there was one, we'll use the method name ``{action}_{method}``... **line 8, 9**: ... otherwise we'll use just the method for the method name. **line 10-13**: We'll get the method from the instance, or respond with a 404 error if there is not such method. **line 14**: Call the method, get the response **line 15, 16**: If the response is just a string, create a full response object from it. **line 19**: and then we forward the request... **line 20**: ... and return the wrapper object we've created. Here's the hello world: .. code-block:: python >>> class Hello(object): ... def __init__(self, req): ... self.request = req ... def get(self): ... return '''
... You're name: ... ...
''' ... def post(self): ... return 'Hello %s!' % self.request.params['name'] >>> hello = rest_controller(Hello) We'll run the same test as before: .. code-block:: python >>> hello_world = Router() >>> hello_world.add_route('/', controller=hello) >>> req = Request.blank('/') >>> resp = req.get_response(hello_world) >>> print resp 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 131
You're name:
>>> req.method = 'POST' >>> req.body = 'name=Ian' >>> resp = req.get_response(hello_world) >>> print resp 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 10 Hello Ian! URL Generation and Request Access --------------------------------- You can use hard-coded links in your HTML, but this can have problems. Relative links are hard to manage, and absolute links presume that your application lives at a particular location. WSGI gives a variable ``SCRIPT_NAME``, which is the portion of the path that led up to this application. If you are writing a blog application, for instance, someone might want to install it at ``/blog/``, and then SCRIPT_NAME would be ``"/blog"``. We should generate links with that in mind. The base URL using SCRIPT_NAME is ``req.application_url``. So, if we have access to the request we can make a URL. But what if we don't have access? We can use thread-local variables to make it easy for any function to get access to the currect request. A "thread-local" variable is a variable whose value is tracked separately for each thread, so if there are multiple requests in different threads, their requests won't clobber each other. The basic means of using a thread-local variable is ``threading.local()``. This creates a blank object that can have thread-local attributes assigned to it. I find the best way to get *at* a thread-local value is with a function, as this makes it clear that you are fetching the object, as opposed to getting at some global object. Here's the basic structure for the local: .. code-block:: python >>> import threading >>> class Localized(object): ... def __init__(self): ... self.local = threading.local() ... def register(self, object): ... self.local.object = object ... def unregister(self): ... del self.local.object ... def __call__(self): ... try: ... return self.local.object ... except AttributeError: ... raise TypeError("No object has been registered for this thread") >>> get_request = Localized() Now we need some *middleware* to register the request object. Middleware is something that wraps an application, possibly modifying the request on the way in or the way out. In a sense the ``Router`` object was middleware, though not exactly because it didn't wrap a single application. This registration middleware looks like: .. code-block:: python >>> class RegisterRequest(object): ... def __init__(self, app): ... self.app = app ... def __call__(self, environ, start_response): ... req = Request(environ) ... get_request.register(req) ... try: ... return self.app(environ, start_response) ... finally: ... get_request.unregister() Now if we do: >>> hello_world = RegisterRequest(hello_world) then the request will be registered each time. Now, lets create a URL generation function: .. code-block:: python >>> import urllib >>> def url(*segments, **vars): ... base_url = get_request().application_url ... path = '/'.join(str(s) for s in segments) ... if not path.startswith('/'): ... path = '/' + path ... if vars: ... path += '?' + urllib.urlencode(vars) ... return base_url + path Now, to test: .. code-block:: python >>> get_request.register(Request.blank('http://localhost/')) >>> url('article', 1) 'http://localhost/article/1' >>> url('search', q='some query') 'http://localhost/search?q=some+query' Templating ---------- Well, we don't *really* need to factor templating into our framework. After all, you return a string from your controller, and you can figure out on your own how to get a rendered string from a template. But we'll add a little helper, because I think it shows a clever trick. We'll use `Tempita `_ for templating, mostly because it's very simplistic about how it does loading. The basic form is: .. code-block:: python import tempita template = tempita.HTMLTemplate.from_filename('some-file.html') But we'll be implementing a function ``render(template_name, **vars)`` that will render the named template, treating it as a path *relative to the location of the render() call*. That's the trick. To do that we use ``sys._getframe``, which is a way to look at information in the calling scope. Generally this is frowned upon, but I think this case is justifiable. We'll also let you pass an instantiated template in instead of a template name, which will be useful in places like a doctest where there aren't other files easily accessible. .. code-block:: python >>> import os >>> import tempita #doctest: +SKIP >>> def render(template, **vars): ... if isinstance(template, basestring): ... caller_location = sys._getframe(1).f_globals['__file__'] ... filename = os.path.join(os.path.dirname(caller_location), template) ... template = tempita.HTMLTemplate.from_filename(filename) ... vars.setdefault('request', get_request()) ... return template.substitute(vars) Conclusion ---------- Well, that's a framework. Ta-da! Of course, this doesn't deal with some other stuff. In particular: * Configuration * Making your routes debuggable * Exception catching and other basic infrastructure * Database connections * Form handling * Authentication But, for now, that's outside the scope of this document. WebOb-1.3.1/docs/reference.txt0000664000175000017500000007433712023145137016753 0ustar chrismchrism00000000000000WebOb Reference +++++++++++++++ .. contents:: .. comment: >>> from doctest import ELLIPSIS Introduction ============ This document covers all the details of the Request and Response objects. It is written to be testable with `doctest `_ -- this effects the flavor of the documentation, perhaps to its detriment. But it also means you can feel confident that the documentation is correct. This is a somewhat different approach to reference documentation compared to the extracted documentation for the `request `_ and `response `_. Request ======= The primary object in WebOb is ``webob.Request``, a wrapper around a `WSGI environment `_. The basic way you create a request object is simple enough: .. code-block:: python >>> from webob import Request >>> environ = {'wsgi.url_scheme': 'http', ...} #doctest: +SKIP >>> req = Request(environ) #doctest: +SKIP (Note that the WSGI environment is a dictionary with a dozen required keys, so it's a bit lengthly to show a complete example of what it would look like -- usually your WSGI server will create it.) The request object *wraps* the environment; it has very little internal state of its own. Instead attributes you access read and write to the environment dictionary. You don't have to understand the details of WSGI to use this library; this library handles those details for you. You also don't have to use this exclusively of other libraries. If those other libraries also keep their state in the environment, multiple wrappers can coexist. Examples of libraries that can coexist include `paste.wsgiwrappers.Request `_ (used by Pylons) and `yaro.Request `_. The WSGI environment has a number of required variables. To make it easier to test and play around with, the ``Request`` class has a constructor that will fill in a minimal environment: .. code-block:: python >>> req = Request.blank('/article?id=1') >>> from pprint import pprint >>> pprint(req.environ) {'HTTP_HOST': 'localhost:80', 'PATH_INFO': '/article', 'QUERY_STRING': 'id=1', 'REQUEST_METHOD': 'GET', 'SCRIPT_NAME': '', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'SERVER_PROTOCOL': 'HTTP/1.0', 'wsgi.errors': ', mode 'w' at ...>, 'wsgi.input': <...IO... object at ...>, 'wsgi.multiprocess': False, 'wsgi.multithread': False, 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.version': (1, 0)} Request Body ------------ ``req.body`` is a file-like object that gives the body of the request (e.g., a POST form, the body of a PUT, etc). It's kind of boring to start, but you can set it to a string and that will be turned into a file-like object. You can read the entire body with ``req.body``. .. code-block:: python >>> hasattr(req.body_file, 'read') True >>> req.body '' >>> req.method = 'PUT' >>> req.body = 'test' >>> hasattr(req.body_file, 'read') True >>> req.body 'test' Method & URL ------------ All the normal parts of a request are also accessible through the request object: .. code-block:: python >>> req.method 'PUT' >>> req.scheme 'http' >>> req.script_name # The base of the URL '' >>> req.script_name = '/blog' # make it more interesting >>> req.path_info # The yet-to-be-consumed part of the URL '/article' >>> req.content_type # Content-Type of the request body '' >>> print req.remote_user # The authenticated user (there is none set) None >>> print req.remote_addr # The remote IP None >>> req.host 'localhost:80' >>> req.host_url 'http://localhost' >>> req.application_url 'http://localhost/blog' >>> req.path_url 'http://localhost/blog/article' >>> req.url 'http://localhost/blog/article?id=1' >>> req.path '/blog/article' >>> req.path_qs '/blog/article?id=1' >>> req.query_string 'id=1' You can make new URLs: .. code-block:: python >>> req.relative_url('archive') 'http://localhost/blog/archive' For parsing the URLs, it is often useful to deal with just the next path segment on PATH_INFO: .. code-block:: python >>> req.path_info_peek() # Doesn't change request 'article' >>> req.path_info_pop() # Does change request! 'article' >>> req.script_name '/blog/article' >>> req.path_info '' Headers ------- All request headers are available through a dictionary-like object ``req.headers``. Keys are case-insensitive. .. code-block:: python >>> req.headers['Content-Type'] = 'application/x-www-urlencoded' >>> sorted(req.headers.items()) [('Content-Length', '4'), ('Content-Type', 'application/x-www-urlencoded'), ('Host', 'localhost:80')] >>> req.environ['CONTENT_TYPE'] 'application/x-www-urlencoded' Query & POST variables ---------------------- Requests can have variables in one of two locations: the query string (``?id=1``), or in the body of the request (generally a POST form). Note that even POST requests can have a query string, so both kinds of variables can exist at the same time. Also, a variable can show up more than once, as in ``?check=a&check=b``. For these variables WebOb uses a `MultiDict `_, which is basically a dictionary wrapper on a list of key/value pairs. It looks like a single-valued dictionary, but you can access all the values of a key with ``.getall(key)`` (which always returns a list, possibly an empty list). You also get all key/value pairs when using ``.items()`` and all values with ``.values()``. Some examples: .. code-block:: python >>> req = Request.blank('/test?check=a&check=b&name=Bob') >>> req.GET MultiDict([(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob')]) >>> req.GET['check'] u'b' >>> req.GET.getall('check') [u'a', u'b'] >>> req.GET.items() [(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob')] We'll have to create a request body and change the method to get POST. Until we do that, the variables are boring: .. code-block:: python >>> req.POST >>> req.POST.items() # NoVars can be read like a dict, but not written [] >>> req.method = 'POST' >>> req.body = 'name=Joe&email=joe@example.com' >>> req.POST MultiDict([(u'name', u'Joe'), (u'email', u'joe@example.com')]) >>> req.POST['name'] u'Joe' Often you won't care where the variables come from. (Even if you care about the method, the location of the variables might not be important.) There is a dictionary called ``req.params`` that contains variables from both sources: .. code-block:: python >>> req.params NestedMultiDict([(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob'), (u'name', u'Joe'), (u'email', u'joe@example.com')]) >>> req.params['name'] u'Bob' >>> req.params.getall('name') [u'Bob', u'Joe'] >>> for name, value in req.params.items(): ... print '%s: %r' % (name, value) check: u'a' check: u'b' name: u'Bob' name: u'Joe' email: u'joe@example.com' The ``POST`` and ``GET`` nomenclature is historical -- ``req.GET`` can be used for non-GET requests to access query parameters, and ``req.POST`` can also be used for PUT requests with the appropriate Content-Type. >>> req = Request.blank('/test?check=a&check=b&name=Bob') >>> req.method = 'PUT' >>> req.body = body = 'var1=value1&var2=value2&rep=1&rep=2' >>> req.environ['CONTENT_LENGTH'] = str(len(req.body)) >>> req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' >>> req.GET MultiDict([(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob')]) >>> req.POST MultiDict([(u'var1', u'value1'), (u'var2', u'value2'), (u'rep', u'1'), (u'rep', u'2')]) Unicode Variables ~~~~~~~~~~~~~~~~~ Submissions are non-unicode (``str``) strings, unless some character set is indicated. A client can indicate the character set with ``Content-Type: application/x-www-form-urlencoded; charset=utf8``, but very few clients actually do this (sometimes XMLHttpRequest requests will do this, as JSON is always UTF8 even when a page is served with a different character set). You can force a charset, which will effect all the variables: .. code-block:: python >>> req.charset = 'utf8' >>> req.GET MultiDict([(u'check', u'a'), (u'check', u'b'), (u'name', u'Bob')]) Cookies ------- Cookies are presented in a simple dictionary. Like other variables, they will be decoded into Unicode strings if you set the charset. .. code-block:: python >>> req.headers['Cookie'] = 'test=value' >>> req.cookies MultiDict([(u'test', u'value')]) Modifying the request --------------------- The headers are all modifiable, as are other environmental variables (like ``req.remote_user``, which maps to ``request.environ['REMOTE_USER']``). If you want to copy the request you can use ``req.copy()``; this copies the ``environ`` dictionary, and the request body from ``environ['wsgi.input']``. The method ``req.remove_conditional_headers(remove_encoding=True)`` can be used to remove headers that might result in a ``304 Not Modified`` response. If you are writing some intermediary it can be useful to avoid these headers. Also if ``remove_encoding`` is true (the default) then any ``Accept-Encoding`` header will be removed, which can result in gzipped responses. Header Getters -------------- In addition to ``req.headers``, there are attributes for most of the request headers defined by the HTTP 1.1 specification. These attributes often return parsed forms of the headers. Accept-* headers ~~~~~~~~~~~~~~~~ There are several request headers that tell the server what the client accepts. These are ``accept`` (the Content-Type that is accepted), ``accept_charset`` (the charset accepted), ``accept_encoding`` (the Content-Encoding, like gzip, that is accepted), and ``accept_language`` (generally the preferred language of the client). The objects returned support containment to test for acceptability. E.g.: .. code-block:: python >>> 'text/html' in req.accept True Because no header means anything is potentially acceptable, this is returning True. We can set it to see more interesting behavior (the example means that ``text/html`` is okay, but ``application/xhtml+xml`` is preferred): .. code-block:: python >>> req.accept = 'text/html;q=0.5, application/xhtml+xml;q=1' >>> req.accept >>> 'text/html' in req.accept True There are a few methods for different strategies of finding a match. .. code-block:: python >>> req.accept.best_match(['text/html', 'application/xhtml+xml']) 'application/xhtml+xml' If we just want to know everything the client prefers, in the order it is preferred: .. code-block:: python >>> req.accept.best_matches() ['application/xhtml+xml', 'text/html'] For languages you'll often have a "fallback" language. E.g., if there's nothing better then use ``en-US`` (and if ``en-US`` is okay, ignore any less preferrable languages): .. code-block:: python >>> req.accept_language = 'es, pt-BR' >>> req.accept_language.best_matches('en-US') ['es', 'pt-BR', 'en-US'] >>> req.accept_language.best_matches('es') ['es'] Conditional Requests ~~~~~~~~~~~~~~~~~~~~ There a number of ways to make a conditional request. A conditional request is made when the client has a document, but it is not sure if the document is up to date. If it is not, it wants a new version. If the document is up to date then it doesn't want to waste the bandwidth, and expects a ``304 Not Modified`` response. ETags are generally the best technique for these kinds of requests; this is an opaque string that indicates the identity of the object. For instance, it's common to use the mtime (last modified) of the file, plus the number of bytes, and maybe a hash of the filename (if there's a possibility that the same URL could point to two different server-side filenames based on other variables). To test if a 304 response is appropriate, you can use: .. code-block:: python >>> server_token = 'opaque-token' >>> server_token in req.if_none_match # You shouldn't return 304 False >>> req.if_none_match = server_token >>> req.if_none_match >>> server_token in req.if_none_match # You *should* return 304 True For date-based comparisons If-Modified-Since is used: .. code-block:: python >>> from webob import UTC >>> from datetime import datetime >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) >>> req.headers['If-Modified-Since'] 'Sun, 01 Jan 2006 12:00:00 GMT' >>> server_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC) >>> req.if_modified_since and req.if_modified_since >= server_modified True For range requests there are two important headers, If-Range (which is form of conditional request) and Range (which requests a range). If the If-Range header fails to match then the full response (not a range) should be returned: .. code-block:: python >>> req.if_range >>> req.if_range.match(etag='some-etag', last_modified=datetime(2005, 1, 1, 12, 0)) True >>> req.if_range = 'opaque-etag' >>> req.if_range.match(etag='other-etag') False >>> req.if_range.match(etag='opaque-etag') True You can also pass in a response object with: .. code-block:: python >>> from webob import Response >>> res = Response(etag='opaque-etag') >>> req.if_range.match_response(res) True To get the range information: >>> req.range = 'bytes=0-100' >>> req.range >>> cr = req.range.content_range(length=1000) >>> cr.start, cr.stop, cr.length (0, 101, 1000) Note that the range headers use *inclusive* ranges (the last byte indexed is included), where Python always uses a range where the last index is excluded from the range. The ``.stop`` index is in the Python form. Another kind of conditional request is a request (typically PUT) that includes If-Match or If-Unmodified-Since. In this case you are saying "here is an update to a resource, but don't apply it if someone else has done something since I last got the resource". If-Match means "do this if the current ETag matches the ETag I'm giving". If-Unmodified-Since means "do this if the resource has remained unchanged". .. code-block:: python >>> server_token in req.if_match # No If-Match means everything is ok True >>> req.if_match = server_token >>> server_token in req.if_match # Still OK True >>> req.if_match = 'other-token' >>> # Not OK, should return 412 Precondition Failed: >>> server_token in req.if_match False For more on this kind of conditional request, see `Detecting the Lost Update Problem Using Unreserved Checkout `_. Calling WSGI Applications ------------------------- The request object can be used to make handy subrequests or test requests against WSGI applications. If you want to make subrequests, you should copy the request (with ``req.copy()``) before sending it to multiple applications, since applications might modify the request when they are run. There's two forms of the subrequest. The more primitive form is this: .. code-block:: python >>> req = Request.blank('/') >>> def wsgi_app(environ, start_response): ... start_response('200 OK', [('Content-type', 'text/plain')]) ... return ['Hi!'] >>> req.call_application(wsgi_app) ('200 OK', [('Content-type', 'text/plain')], ['Hi!']) Note it returns ``(status_string, header_list, app_iter)``. If ``app_iter.close()`` exists, it is your responsibility to call it. A handier response can be had with: .. code-block:: python >>> res = req.get_response(wsgi_app) >>> res >>> res.status '200 OK' >>> res.headers ResponseHeaders([('Content-type', 'text/plain')]) >>> res.body 'Hi!' You can learn more about this response object in the Response_ section. Ad-Hoc Attributes ----------------- You can assign attributes to your request objects. They will all go in ``environ['webob.adhoc_attrs']`` (a dictionary). .. code-block:: python >>> req = Request.blank('/') >>> req.some_attr = 'blah blah blah' >>> new_req = Request(req.environ) >>> new_req.some_attr 'blah blah blah' >>> req.environ['webob.adhoc_attrs'] {'some_attr': 'blah blah blah'} Response ======== The ``webob.Response`` object contains everything necessary to make a WSGI response. Instances of it are in fact WSGI applications, but it can also represent the result of calling a WSGI application (as noted in `Calling WSGI Applications`_). It can also be a way of accumulating a response in your WSGI application. A WSGI response is made up of a status (like ``200 OK``), a list of headers, and a body (or iterator that will produce a body). Core Attributes --------------- The core attributes are unsurprising: .. code-block:: python >>> from webob import Response >>> res = Response() >>> res.status '200 OK' >>> res.headerlist [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '0')] >>> res.body '' You can set any of these attributes, e.g.: .. code-block:: python >>> res.status = 404 >>> res.status '404 Not Found' >>> res.status_code 404 >>> res.headerlist = [('Content-type', 'text/html')] >>> res.body = 'test' >>> print res 404 Not Found Content-type: text/html Content-Length: 4 test >>> res.body = u"test" Traceback (most recent call last): ... TypeError: You cannot set Response.body to a unicode object (use Response.text) >>> res.text = u"test" Traceback (most recent call last): ... AttributeError: You cannot access Response.text unless charset is set >>> res.charset = 'utf8' >>> res.text = u"test" >>> res.body 'test' You can set any attribute with the constructor, like ``Response(charset='utf8')`` Headers ------- In addition to ``res.headerlist``, there is dictionary-like view on the list in ``res.headers``: .. code-block:: python >>> res.headers ResponseHeaders([('Content-Type', 'text/html; charset=utf8'), ('Content-Length', '4')]) This is case-insensitive. It can support multiple values for a key, though only if you use ``res.headers.add(key, value)`` or read them with ``res.headers.getall(key)``. Body & app_iter --------------- The ``res.body`` attribute represents the entire body of the request as a single string (not unicode, though you can set it to unicode if you have a charset defined). There is also a ``res.app_iter`` attribute that reprsents the body as an iterator. WSGI applications return these ``app_iter`` iterators instead of strings, and sometimes it can be problematic to load the entire iterator at once (for instance, if it returns the contents of a very large file). Generally it is not a problem, and often the iterator is something simple like a one-item list containing a string with the entire body. If you set the body then Content-Length will also be set, and an ``res.app_iter`` will be created for you. If you set ``res.app_iter`` then Content-Length will be cleared, but it won't be set for you. There is also a file-like object you can access, which will update the app_iter in-place (turning the app_iter into a list if necessary): .. code-block:: python >>> res = Response(content_type='text/plain', charset=None) >>> f = res.body_file >>> f.write('hey') >>> f.write(u'test') Traceback (most recent call last): . . . TypeError: You can only write unicode to Response if charset has been set >>> f.encoding >>> res.charset = 'utf8' >>> f.encoding 'utf8' >>> f.write(u'test') >>> res.app_iter ['', 'hey', 'test'] >>> res.body 'heytest' Header Getters -------------- Like Request, HTTP response headers are also available as individual properties. These represent parsed forms of the headers. Content-Type is a special case, as the type and the charset are handled through two separate properties: .. code-block:: python >>> res = Response() >>> res.content_type = 'text/html' >>> res.charset = 'utf8' >>> res.content_type 'text/html' >>> res.headers['content-type'] 'text/html; charset=utf8' >>> res.content_type = 'application/atom+xml' >>> res.content_type_params {'charset': 'utf8'} >>> res.content_type_params = {'type': 'entry', 'charset': 'utf8'} >>> res.headers['content-type'] 'application/atom+xml; charset=utf8; type=entry' Other headers: .. code-block:: python >>> # Used with a redirect: >>> res.location = 'http://localhost/foo' >>> # Indicates that the server accepts Range requests: >>> res.accept_ranges = 'bytes' >>> # Used by caching proxies to tell the client how old the >>> # response is: >>> res.age = 120 >>> # Show what methods the client can do; typically used in >>> # a 405 Method Not Allowed response: >>> res.allow = ['GET', 'PUT'] >>> # Set the cache-control header: >>> res.cache_control.max_age = 360 >>> res.cache_control.no_transform = True >>> # Tell the browser to treat the response as an attachment: >>> res.content_disposition = 'attachment; filename=foo.xml' >>> # Used if you had gzipped the body: >>> res.content_encoding = 'gzip' >>> # What language(s) are in the content: >>> res.content_language = ['en'] >>> # Seldom used header that tells the client where the content >>> # is from: >>> res.content_location = 'http://localhost/foo' >>> # Seldom used header that gives a hash of the body: >>> res.content_md5 = 'big-hash' >>> # Means we are serving bytes 0-500 inclusive, out of 1000 bytes total: >>> # you can also use the range setter shown earlier >>> res.content_range = (0, 501, 1000) >>> # The length of the content; set automatically if you set >>> # res.body: >>> res.content_length = 4 >>> # Used to indicate the current date as the server understands >>> # it: >>> res.date = datetime.now() >>> # The etag: >>> res.etag = 'opaque-token' >>> # You can generate it from the body too: >>> res.md5_etag() >>> res.etag '1B2M2Y8AsgTpgAmY7PhCfg' >>> # When this page should expire from a cache (Cache-Control >>> # often works better): >>> import time >>> res.expires = time.time() + 60*60 # 1 hour >>> # When this was last modified, of course: >>> res.last_modified = datetime(2007, 1, 1, 12, 0, tzinfo=UTC) >>> # Used with 503 Service Unavailable to hint the client when to >>> # try again: >>> res.retry_after = 160 >>> # Indicate the server software: >>> res.server = 'WebOb/1.0' >>> # Give a list of headers that the cache should vary on: >>> res.vary = ['Cookie'] Note in each case you can general set the header to a string to avoid any parsing, and set it to None to remove the header (or do something like ``del res.vary``). In the case of date-related headers you can set the value to a ``datetime`` instance (ideally with a UTC timezone), a time tuple, an integer timestamp, or a properly-formatted string. After setting all these headers, here's the result: .. code-block:: python >>> for name, value in res.headerlist: ... print '%s: %s' % (name, value) Content-Type: application/atom+xml; charset=utf8; type=entry Location: http://localhost/foo Accept-Ranges: bytes Age: 120 Allow: GET, PUT Cache-Control: max-age=360, no-transform Content-Disposition: attachment; filename=foo.xml Content-Encoding: gzip Content-Language: en Content-Location: http://localhost/foo Content-MD5: big-hash Content-Range: bytes 0-500/1000 Content-Length: 4 Date: ... GMT ETag: ... Expires: ... GMT Last-Modified: Mon, 01 Jan 2007 12:00:00 GMT Retry-After: 160 Server: WebOb/1.0 Vary: Cookie You can also set Cache-Control related attributes with ``req.cache_expires(seconds, **attrs)``, like: .. code-block:: python >>> res = Response() >>> res.cache_expires(10) >>> res.headers['Cache-Control'] 'max-age=10' >>> res.cache_expires(0) >>> res.headers['Cache-Control'] 'max-age=0, must-revalidate, no-cache, no-store' >>> res.headers['Expires'] '... GMT' You can also use the `timedelta `_ constants defined, e.g.: .. code-block:: python >>> from webob import * >>> res = Response() >>> res.cache_expires(2*day+4*hour) >>> res.headers['Cache-Control'] 'max-age=187200' Cookies ------- Cookies (and the Set-Cookie header) are handled with a couple methods. Most importantly: .. code-block:: python >>> res.set_cookie('key', 'value', max_age=360, path='/', ... domain='example.org', secure=True) >>> res.headers['Set-Cookie'] 'key=value; Domain=example.org; Max-Age=360; Path=/; expires=... GMT; secure' >>> # To delete a cookie previously set in the client: >>> res.delete_cookie('bad_cookie') >>> res.headers['Set-Cookie'] 'bad_cookie=; Max-Age=0; Path=/; expires=... GMT' The only other real method of note (note that this does *not* delete the cookie from clients, only from the response object): .. code-block:: python >>> res.unset_cookie('key') >>> res.unset_cookie('bad_cookie') >>> print res.headers.get('Set-Cookie') None Binding a Request ----------------- You can bind a request (or request WSGI environ) to the response object. This is available through ``res.request`` or ``res.environ``. This is currently only used in setting ``res.location``, to make the location absolute if necessary. Response as a WSGI application ------------------------------ A response is a WSGI application, in that you can do: .. code-block:: python >>> req = Request.blank('/') >>> status, headers, app_iter = req.call_application(res) A possible pattern for your application might be: .. code-block:: python >>> def my_app(environ, start_response): ... req = Request(environ) ... res = Response() ... res.content_type = 'text/plain' ... parts = [] ... for name, value in sorted(req.environ.items()): ... parts.append('%s: %r' % (name, value)) ... res.body = '\n'.join(parts) ... return res(environ, start_response) >>> req = Request.blank('/') >>> res = req.get_response(my_app) >>> print res 200 OK Content-Type: text/plain; charset=UTF-8 Content-Length: ... HTTP_HOST: 'localhost:80' PATH_INFO: '/' QUERY_STRING: '' REQUEST_METHOD: 'GET' SCRIPT_NAME: '' SERVER_NAME: 'localhost' SERVER_PORT: '80' SERVER_PROTOCOL: 'HTTP/1.0' wsgi.errors: ', mode 'w' at ...> wsgi.input: <...IO... object at ...> wsgi.multiprocess: False wsgi.multithread: False wsgi.run_once: False wsgi.url_scheme: 'http' wsgi.version: (1, 0) Exceptions ========== In addition to Request and Response objects, there are a set of Python exceptions for different HTTP responses (3xx, 4xx, 5xx codes). These provide a simple way to provide these non-200 response. A very simple body is provided. .. code-block:: python >>> from webob.exc import * >>> exc = HTTPTemporaryRedirect(location='foo') >>> req = Request.blank('/path/to/something') >>> print str(req.get_response(exc)).strip() 307 Temporary Redirect Location: http://localhost/path/to/foo Content-Length: 126 Content-Type: text/plain; charset=UTF-8 307 Temporary Redirect The resource has been moved to http://localhost/path/to/foo; you should be redirected automatically. Note that only if there's an ``Accept: text/html`` header in the request will an HTML response be given: .. code-block:: python >>> req.accept += 'text/html' >>> print str(req.get_response(exc)).strip() 307 Temporary Redirect Location: http://localhost/path/to/foo Content-Length: 270 Content-Type: text/html; charset=UTF-8 307 Temporary Redirect

307 Temporary Redirect

The resource has been moved to http://localhost/path/to/foo; you should be redirected automatically. This is taken from `paste.httpexceptions `_, and if you have Paste installed then these exceptions will be subclasses of the Paste exceptions. Conditional WSGI Application ---------------------------- The Response object can handle your conditional responses for you, checking If-None-Match, If-Modified-Since, and Range/If-Range. To enable this you must create the response like ``Response(conditional_request=True)``, or make a subclass like: .. code-block:: python >>> class AppResponse(Response): ... default_content_type = 'text/html' ... default_conditional_response = True >>> res = AppResponse(body='0123456789', ... last_modified=datetime(2005, 1, 1, 12, 0, tzinfo=UTC)) >>> req = Request.blank('/') >>> req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) >>> req.get_response(res) >>> del req.if_modified_since >>> res.etag = 'opaque-tag' >>> req.if_none_match = 'opaque-tag' >>> req.get_response(res) >>> req.if_none_match = '*' >>> 'x' in req.if_none_match True >>> req.if_none_match = req.if_none_match >>> 'x' in req.if_none_match True >>> req.if_none_match = None >>> 'x' in req.if_none_match False >>> req.if_match = None >>> 'x' in req.if_match True >>> req.if_match = req.if_match >>> 'x' in req.if_match True >>> req.headers.get('If-Match') '*' >>> del req.if_none_match >>> req.range = (1, 5) >>> result = req.get_response(res) >>> result.headers['content-range'] 'bytes 1-4/10' >>> result.body '1234' WebOb-1.3.1/docs/test_request.txt0000664000175000017500000003744012023145137017536 0ustar chrismchrism00000000000000This demonstrates how the Request object works, and tests it. You can instantiate a request using ``Request.blank()``, to create a fresh environment dictionary with all the basic keys such a dictionary should have. >>> import sys >>> if sys.version >= '2.7': ... from io import BytesIO as InputType ... else: ... from cStringIO import InputType >>> from doctest import ELLIPSIS, NORMALIZE_WHITESPACE >>> from webob import Request, UTC >>> req = Request.blank('/') >>> req # doctest: +ELLIPSIS >>> print req GET / HTTP/1.0 Host: localhost:80 >>> req.environ # doctest: +ELLIPSIS {...} >>> isinstance(req.body_file, InputType) True >>> req.scheme 'http' >>> req.method 'GET' >>> req.script_name '' >>> req.path_info '/' >>> req.upath_info u'/' >>> req.content_type '' >>> print req.remote_user None >>> req.host_url 'http://localhost' >>> req.script_name = '/foo' >>> req.path_info = '/bar/' >>> req.environ['QUERY_STRING'] = 'a=b' >>> req.application_url 'http://localhost/foo' >>> req.path_url 'http://localhost/foo/bar/' >>> req.url 'http://localhost/foo/bar/?a=b' >>> req.relative_url('baz') 'http://localhost/foo/bar/baz' >>> req.relative_url('baz', to_application=True) 'http://localhost/foo/baz' >>> req.relative_url('http://example.org') 'http://example.org' >>> req.path_info_peek() 'bar' >>> req.path_info_pop() 'bar' >>> req.script_name, req.path_info ('/foo/bar', '/') >>> print req.environ.get('wsgiorg.routing_args') None >>> req.urlvars {} >>> req.environ['wsgiorg.routing_args'] ((), {}) >>> req.urlvars = dict(x='y') >>> req.environ['wsgiorg.routing_args'] ((), {'x': 'y'}) >>> req.urlargs () >>> req.urlargs = (1, 2, 3) >>> req.environ['wsgiorg.routing_args'] ((1, 2, 3), {'x': 'y'}) >>> del req.urlvars >>> req.environ['wsgiorg.routing_args'] ((1, 2, 3), {}) >>> req.urlvars = {'test': 'value'} >>> del req.urlargs >>> req.environ['wsgiorg.routing_args'] ((), {'test': 'value'}) >>> req.is_xhr False >>> req.environ['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' >>> req.is_xhr True >>> req.host 'localhost:80' There are also variables to access the variables and body: >>> from cStringIO import StringIO >>> body = 'var1=value1&var2=value2&rep=1&rep=2' >>> req = Request.blank('/') >>> req.method = 'POST' >>> req.body_file = StringIO(body) >>> req.environ['CONTENT_LENGTH'] = str(len(body)) >>> req.POST MultiDict([(u'var1', u'value1'), (u'var2', u'value2'), (u'rep', u'1'), (u'rep', u'2')]) Note that the variables are there for GET requests and non-form requests, but they are empty and read-only: >>> req = Request.blank('/') >>> req.POST >>> req.POST.items() [] >>> req.POST['x'] = 'y' Traceback (most recent call last): ... KeyError: 'Cannot add variables: Not a form request' >>> req.method = 'POST' >>> req.POST MultiDict([]) >>> req.content_type = 'text/xml' >>> req.body_file = StringIO('') >>> req.POST >>> req.body '' You can also get access to the query string variables, of course: >>> req = Request.blank('/?a=b&d=e&d=f') >>> req.GET MultiDict([(u'a', u'b'), (u'd', u'e'), (u'd', u'f')]) >>> req.GET['d'] u'f' >>> req.GET.getall('d') [u'e', u'f'] >>> req.method = 'POST' >>> req.body = 'x=y&d=g' >>> req.environ['CONTENT_LENGTH'] '7' >>> req.params NestedMultiDict([(u'a', u'b'), (u'd', u'e'), (u'd', u'f'), (u'x', u'y'), (u'd', u'g')]) >>> req.params['d'] u'f' >>> req.params.getall('d') [u'e', u'f', u'g'] Cookies are viewed as a dictionary (*view only*): >>> req = Request.blank('/') >>> req.environ['HTTP_COOKIE'] = 'var1=value1; var2=value2' >>> sorted(req.cookies.items()) [(u'var1', u'value1'), (u'var2', u'value2')] Sometimes conditional headers are problematic. You can remove them: >>> from datetime import datetime >>> req = Request.blank('/') >>> req.if_none_match = 'some-etag' >>> req.if_modified_since = datetime(2005, 1, 1, 12, 0) >>> req.environ['HTTP_ACCEPT_ENCODING'] = 'gzip' >>> print sorted(req.headers.items()) [('Accept-Encoding', 'gzip'), ('Host', 'localhost:80'), ('If-Modified-Since', 'Sat, 01 Jan 2005 12:00:00 GMT'), ('If-None-Match', 'some-etag')] >>> req.remove_conditional_headers() >>> print req.headers {'Host': 'localhost:80'} Some headers are handled specifically (more should be added): >>> req = Request.blank('/') >>> req.if_none_match = 'xxx' >>> 'xxx' in req.if_none_match True >>> 'yyy' in req.if_none_match False >>> req.if_modified_since = datetime(2005, 1, 1, 12, 0) >>> req.if_modified_since < datetime(2006, 1, 1, 12, 0, tzinfo=UTC) True >>> req.user_agent is None True >>> req.user_agent = 'MSIE-Win' >>> req.user_agent 'MSIE-Win' >>> req.cache_control >>> req.cache_control.no_cache = True >>> req.cache_control.max_age = 0 >>> req.cache_control .cache_control is a view: >>> 'cache-control' in req.headers True >>> req.headers['cache-control'] 'max-age=0, no-cache' >>> req.cache_control = {'no-transform': None, 'max-age': 100} >>> req.headers['cache-control'] 'max-age=100, no-transform' Accept-* headers are parsed into read-only objects that support containment tests, and some useful methods. Note that parameters on mime types are not supported. >>> req = Request.blank('/') >>> req.environ['HTTP_ACCEPT'] = "text/*;q=0.3, text/html;q=0.7, text/html;level=1, text/html;level=2;q=0.4, */*;q=0.1" >>> req.accept # doctest: +ELLIPSIS >>> for item, quality in req.accept._parsed: ... print '%s: %0.1f' % (item, quality) text/*: 0.3 text/html: 0.7 text/html: 1.0 text/html: 0.4 */*: 0.1 >>> '%0.1f' % req.accept.quality('text/plain') '0.3' >>> '%0.1f' % req.accept.quality('text/html') '1.0' >>> 'image/png' in req.accept True >>> req.environ['HTTP_ACCEPT'] = "text/html, application/xml; q=0.7, text/*; q=0.5, */*; q=0.1" >>> req.accept # doctest: +ELLIPSIS >>> req.accept.best_match(['text/plain', 'application/xml']) 'application/xml' >>> req.accept = "text/html, application/xml, text/*; q=0.5" >>> 'image/png' in req.accept False >>> 'text/plain' in req.accept True >>> req.accept_charset = 'utf8' >>> 'UTF8' in req.accept_charset True >>> 'gzip' in req.accept_encoding False >>> req.accept_encoding = 'gzip' >>> 'GZIP' in req.accept_encoding True >>> req.accept_language = {'en-US': 0.5, 'es': 0.7} >>> str(req.accept_language) 'es;q=0.7, en-US;q=0.5' >>> req.headers['Accept-Language'] 'es;q=0.7, en-US;q=0.5' >>> req.accept_language.best_matches('en-GB') ['es', 'en-US', 'en-GB'] >>> req.accept_language.best_matches('es') ['es'] >>> req.accept_language.best_matches('ES') ['ES'] >>> req = Request.blank('/', accept_language='en;q=0.5') >>> req.accept_language.best_match(['en-gb']) 'en-gb' >>> req = Request.blank('/', accept_charset='utf-8;q=0.5') >>> req.accept_charset.best_match(['iso-8859-1', 'utf-8']) 'iso-8859-1' The If-Range header is a combination of a possible conditional date or etag match:: >>> req = Request.blank('/') >>> req.if_range = 'asdf' >>> req.if_range >>> from webob import Response >>> res = Response() >>> res.etag = 'asdf' >>> req.if_range.match_response(res) True >>> res.etag = None >>> req.if_range.match_response(res) False >>> res.last_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC) >>> req.if_range = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) >>> req.if_range >>> req.if_range.match_response(res) True >>> res.last_modified = datetime(2007, 1, 1, 12, 0, tzinfo=UTC) >>> req.if_range.match_response(res) False >>> req = Request.blank('/') >>> req.if_range >>> req.if_range.match_response(res) True Ranges work like so:: >>> req = Request.blank('/') >>> req.range = (0, 100) >>> req.range >>> str(req.range) 'bytes=0-99' You can use them with responses:: >>> res = Response() >>> res.content_range = req.range.content_range(1000) >>> res.content_range >>> str(res.content_range) 'bytes 0-99/1000' >>> start, end, length = res.content_range >>> start, end, length (0, 100, 1000) A quick test of caching the request body: >>> from cStringIO import StringIO >>> length = Request.request_body_tempfile_limit+10 >>> data = StringIO('x'*length) >>> req = Request.blank('/') >>> req.content_length = length >>> req.method = 'PUT' >>> req.body_file = data >>> req.body_file_raw <...IO... object at ...> >>> len(req.body) 10250 >>> req.body_file >>> int(req.body_file.tell()) 0 >>> req.POST >>> int(req.body_file.tell()) 0 Some query tests: >>> req = Request.blank('/') >>> req.GET.get('unknown') >>> req.GET.get('unknown', '?') '?' >>> req.POST.get('unknown') >>> req.POST.get('unknown', '?') '?' >>> req.params.get('unknown') >>> req.params.get('unknown', '?') '?' Some updating of the query string: >>> req = Request.blank('http://localhost/foo?a=b') >>> req.GET MultiDict([(u'a', u'b')]) >>> req.GET['c'] = 'd' >>> req.query_string 'a=b&c=d' And for dealing with file uploads: >>> req = Request.blank('/posty') >>> req.method = 'POST' >>> req.content_type = 'multipart/form-data; boundary="foobar"' >>> req.body = '''\ ... --foobar ... Content-Disposition: form-data; name="a" ... ... b ... --foobar ... Content-Disposition: form-data; name="upload"; filename="test.html" ... Content-Type: text/html ... ... Some text... ... --foobar-- ... ''' >>> req.POST MultiDict([(u'a', u'b'), (u'upload', FieldStorage(u'upload', u'test.html'))]) >>> print req.body.replace('\r', '') # doctest: +REPORT_UDIFF --foobar Content-Disposition: form-data; name="a" b --foobar Content-Disposition: form-data; name="upload"; filename="test.html" Content-type: text/html Some text... --foobar-- >>> req.POST['c'] = 'd' >>> req.POST MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html')), ('c', 'd')]) >>> req.body_file_raw <....BufferedReader> >>> req.body_file_raw.raw >>> sorted(req.POST.keys()) ['a', 'c', 'upload'] >>> print req.body.replace('\r', '') # doctestx: +REPORT_UDIFF --foobar Content-Disposition: form-data; name="a" b --foobar Content-Disposition: form-data; name="upload"; filename="test.html" Content-type: text/html Some text... --foobar Content-Disposition: form-data; name="c" d --foobar-- FakeCGIBody have both readline and readlines methods: >>> req_ = Request.blank('/posty') >>> req_.method = 'POST' >>> req_.content_type = 'multipart/form-data; boundary="foobar"' >>> req_.body = '''\ ... --foobar ... Content-Disposition: form-data; name="a" ... ... b ... --foobar ... Content-Disposition: form-data; name="upload"; filename="test.html" ... Content-Type: text/html ... ... Some text... ... --foobar-- ... ''' >>> req_.POST MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html'))]) >>> print req_.body.replace('\r', '') # doctest: +REPORT_UDIFF --foobar Content-Disposition: form-data; name="a" b --foobar Content-Disposition: form-data; name="upload"; filename="test.html" Content-type: text/html Some text... --foobar-- >>> req_.POST['c'] = 'd' >>> req_.POST MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html')), ('c', 'd')]) >>> req_.body_file_raw.readline() '--foobar\r\n' >>> [n.replace('\r', '') for n in req_.body_file.readlines()] ['Content-Disposition: form-data; name="a"\n', '\n', 'b\n', '--foobar\n', 'Content-Disposition: form-data; name="upload"; filename="test.html"\n', 'Content-type: text/html\n', '\n', 'Some text...\n', '--foobar\n', 'Content-Disposition: form-data; name="c"\n', '\n', 'd\n', '--foobar--'] Also reparsing works through the fake body: >>> del req.environ['webob._parsed_post_vars'] >>> req.POST MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html')), ('c', 'd')]) A ``BaseRequest`` class exists for the purpose of usage by web frameworks that want a less featureful ``Request``. For example, the ``Request`` class mutates the ``environ['webob.adhoc_attrs']`` attribute when its ``__getattr__``, ``__setattr__``, and ``__delattr__`` are invoked. The ``BaseRequest`` class omits the mutation annotation behavior provided by the default ``Request`` implementation. Instead, the of the ``BaseRequest`` class actually mutates the ``__dict__`` of the request instance itself. >>> from webob import BaseRequest >>> req = BaseRequest.blank('/') >>> req.foo = 1 >>> req.environ['webob.adhoc_attrs'] Traceback (most recent call last): ... KeyError: 'webob.adhoc_attrs' >>> req.foo 1 >>> del req.foo >>> req.foo Traceback (most recent call last): ... AttributeError: 'BaseRequest' object has no attribute 'foo' >>> req = BaseRequest.blank('//foo') >>> print req.path_info_pop('x') None >>> req.script_name '' >>> print BaseRequest.blank('/foo').path_info_pop('/') None >>> BaseRequest.blank('/foo').path_info_pop('foo') 'foo' >>> BaseRequest.blank('/foo').path_info_pop('fo+') 'foo' >>> BaseRequest.blank('//1000').path_info_pop('\d+') '1000' >>> BaseRequest.blank('/1000/x').path_info_pop('\d+') '1000' >>> req = Request.blank('/', method='PUT', body='x'*10) str(request) returns the request as HTTP request string >>> print req PUT / HTTP/1.0 Content-Length: 10 Host: localhost:80 xxxxxxxxxx req.as_text() does the same thing but also can take additional argument `skip_body` skip_body=True excludes the body from the result >>> print req.as_text(skip_body=True) PUT / HTTP/1.0 Content-Length: 10 Host: localhost:80 req.as_bytes() returns the body as bytes; it does the same thing as as_text except the result is bytes. >>> print req.as_bytes(skip_body=True) PUT / HTTP/1.0 Content-Length: 10 Host: localhost:80 skip_body= excludes the body from the result if it's longer than that number >>> print req.as_bytes(skip_body=5) PUT / HTTP/1.0 Content-Length: 10 Host: localhost:80 but not if it's shorter >>> print req.as_bytes(skip_body=100) PUT / HTTP/1.0 Content-Length: 10 Host: localhost:80 xxxxxxxxxx WebOb-1.3.1/docs/modules/0000775000175000017500000000000012252637046015717 5ustar chrismchrism00000000000000WebOb-1.3.1/docs/modules/client.txt0000664000175000017500000000033712023145137017730 0ustar chrismchrism00000000000000:mod:`webob.client` -- Send WSGI requests over HTTP =================================================== .. automodule:: webob.client Client ------ .. autoclass:: SendRequest :members: .. autoclass:: send_request_app WebOb-1.3.1/docs/modules/webob.txt0000664000175000017500000000376012250507612017555 0ustar chrismchrism00000000000000:mod:`webob` -- Request/Response objects ======================================== Request ------- .. autoclass:: webob.request.BaseRequest :members: Response -------- .. autoclass:: webob.response.Response :members: .. autoclass:: webob.response.AppIterRange :members: Headers ------- Accept-* ~~~~~~~~ .. automodule:: webob.acceptparse .. autoclass:: Accept :members: .. autoclass:: MIMEAccept :members: Cache-Control ~~~~~~~~~~~~~ .. autoclass:: webob.cachecontrol.CacheControl :members: Range and related headers ~~~~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: webob.byterange.Range :members: .. autoclass:: webob.byterange.ContentRange :members: .. autoclass:: webob.etag.IfRange :members: ETag ~~~~ .. autoclass:: webob.etag.ETagMatcher :members: Cookies ~~~~~~~ .. autoclass:: webob.cookies.CookieProfile :members: .. autoclass:: webob.cookies.SignedCookieProfile :members: .. autoclass:: webob.cookies.SignedSerializer :members: .. autoclass:: webob.cookies.JSONSerializer :members: .. autofunction:: webob.cookies.make_cookie Misc Functions and Internals ---------------------------- .. autofunction:: webob.html_escape .. comment: not sure what to do with these constants; not autoclass .. autoclass:: webob.day .. autoclass:: webob.week .. autoclass:: webob.hour .. autoclass:: webob.minute .. autoclass:: webob.second .. autoclass:: webob.month .. autoclass:: webob.year .. autoclass:: webob.headers.ResponseHeaders :members: .. autoclass:: webob.headers.EnvironHeaders :members: .. automodule:: webob.multidict .. autoclass:: MultiDict :members: .. autoclass:: NestedMultiDict :members: .. autoclass:: NoVars :members: .. autoclass:: webob.cachecontrol.UpdateDict :members: .. comment: Descriptors ----------- .. autoclass:: webob.descriptors.environ_getter .. autoclass:: webob.descriptors.header_getter .. autoclass:: webob.descriptors.converter .. autoclass:: webob.descriptors.deprecated_property WebOb-1.3.1/docs/modules/dec.txt0000644000175000017500000000023611636566135017217 0ustar chrismchrism00000000000000:mod:`webob.dec` -- WSGIfy decorator ==================================== .. automodule:: webob.dec Decorator --------- .. autoclass:: wsgify :members: WebOb-1.3.1/docs/modules/static.txt0000644000175000017500000000034111743245714017744 0ustar chrismchrism00000000000000:mod:`webob.static` -- Serving static files =========================================== .. automodule:: webob.static .. autoclass:: webob.static.FileApp :members: .. autoclass:: webob.static.DirectoryApp :members: WebOb-1.3.1/docs/test_response.txt0000644000175000017500000003162211636566135017714 0ustar chrismchrism00000000000000This demonstrates how the Response object works, and tests it at the same time. >>> from doctest import ELLIPSIS >>> from webob import Response, UTC >>> from datetime import datetime >>> res = Response('Test', status='200 OK') This is a minimal response object. We can do things like get and set the body: >>> res.body 'Test' >>> res.body = 'Another test' >>> res.body 'Another test' >>> res.body = 'Another' >>> res.write(' test') >>> res.app_iter ['Another', ' test'] >>> res.content_length 12 >>> res.headers['content-length'] '12' Content-Length is only applied when setting the body to a string; you have to set it manually otherwise. There are also getters and setters for the various pieces: >>> res.app_iter = ['test'] >>> print res.content_length None >>> res.content_length = 4 >>> res.status '200 OK' >>> res.status_int 200 >>> res.headers ResponseHeaders([('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '4')]) >>> res.headerlist [('Content-Type', 'text/html; charset=UTF-8'), ('Content-Length', '4')] Content-type and charset are handled separately as properties, though they are both in the ``res.headers['content-type']`` header: >>> res.content_type 'text/html' >>> res.content_type = 'text/html' >>> res.content_type 'text/html' >>> res.charset 'UTF-8' >>> res.charset = 'iso-8859-1' >>> res.charset 'iso-8859-1' >>> res.content_type 'text/html' >>> res.headers['content-type'] 'text/html; charset=iso-8859-1' Cookie handling is done through methods: >>> res.set_cookie('test', 'value') >>> res.headers['set-cookie'] 'test=value; Path=/' >>> res.set_cookie('test2', 'value2', max_age=10000) >>> res.headers['set-cookie'] # We only see the last header 'test2=value2; Max-Age=10000; Path=/; expires=... GMT' >>> res.headers.getall('set-cookie') ['test=value; Path=/', 'test2=value2; Max-Age=10000; Path=/; expires=... GMT'] >>> res.unset_cookie('test') >>> res.headers.getall('set-cookie') ['test2=value2; Max-Age=10000; Path=/; expires=... GMT'] >>> res.set_cookie('test2', 'value2-add') >>> res.headers.getall('set-cookie') ['test2=value2; Max-Age=10000; Path=/; expires=... GMT', 'test2=value2-add; Path=/'] >>> res.set_cookie('test2', 'value2-replace', overwrite=True) >>> res.headers.getall('set-cookie') ['test2=value2-replace; Path=/'] >>> r = Response() >>> r.set_cookie('x', 'x') >>> r.set_cookie('y', 'y') >>> r.set_cookie('z', 'z') >>> r.headers.getall('set-cookie') ['x=x; Path=/', 'y=y; Path=/', 'z=z; Path=/'] >>> r.unset_cookie('y') >>> r.headers.getall('set-cookie') ['x=x; Path=/', 'z=z; Path=/'] Most headers are available in a parsed getter/setter form through properties: >>> res.age = 10 >>> res.age, res.headers['age'] (10, '10') >>> res.allow = ['GET', 'PUT'] >>> res.allow, res.headers['allow'] (('GET', 'PUT'), 'GET, PUT') >>> res.cache_control >>> print res.cache_control.max_age None >>> res.cache_control.properties['max-age'] = None >>> print res.cache_control.max_age -1 >>> res.cache_control.max_age = 10 >>> res.cache_control >>> res.headers['cache-control'] 'max-age=10' >>> res.cache_control.max_stale = 10 Traceback (most recent call last): ... AttributeError: The property max-stale only applies to request Cache-Control >>> res.cache_control = {} >>> res.cache_control >>> res.content_disposition = 'attachment; filename=foo.xml' >>> (res.content_disposition, res.headers['content-disposition']) ('attachment; filename=foo.xml', 'attachment; filename=foo.xml') >>> res.content_encoding = 'gzip' >>> (res.content_encoding, res.headers['content-encoding']) ('gzip', 'gzip') >>> res.content_language = 'en' >>> (res.content_language, res.headers['content-language']) (('en',), 'en') >>> res.content_location = 'http://localhost:8080' >>> res.headers['content-location'] 'http://localhost:8080' >>> res.content_range = (0, 100, 1000) >>> (res.content_range, res.headers['content-range']) (, 'bytes 0-99/1000') >>> res.date = datetime(2005, 1, 1, 12, 0, tzinfo=UTC) >>> (res.date, res.headers['date']) (datetime.datetime(2005, 1, 1, 12, 0, tzinfo=UTC), 'Sat, 01 Jan 2005 12:00:00 GMT') >>> print res.etag None >>> res.etag = 'foo' >>> (res.etag, res.headers['etag']) ('foo', '"foo"') >>> res.etag = 'something-with-"quotes"' >>> (res.etag, res.headers['etag']) ('something-with-"quotes"', '"something-with-\\"quotes\\""') >>> res.expires = res.date >>> res.retry_after = 120 # two minutes >>> res.retry_after datetime.datetime(...) >>> res.server = 'Python/foo' >>> res.headers['server'] 'Python/foo' >>> res.vary = ['Cookie'] >>> (res.vary, res.headers['vary']) (('Cookie',), 'Cookie') The location header will absolutify itself when the response application is actually served. We can force this with ``req.get_response``:: >>> res.location = '/test.html' >>> from webob import Request >>> req = Request.blank('/') >>> res.location '/test.html' >>> req.get_response(res).location 'http://localhost/test.html' >>> res.location = '/test2.html' >>> req.get_response(res).location 'http://localhost/test2.html' There's some conditional response handling too (you have to turn on conditional_response):: >>> res = Response('abc', conditional_response=True) # doctest: +ELLIPSIS >>> req = Request.blank('/') >>> res.etag = 'tag' >>> req.if_none_match = 'tag' >>> req.get_response(res) >>> res.etag = 'other-tag' >>> req.get_response(res) >>> del req.if_none_match >>> req.if_modified_since = datetime(2005, 1, 1, 12, 1, tzinfo=UTC) >>> res.last_modified = datetime(2005, 1, 1, 12, 1, tzinfo=UTC) >>> print req.get_response(res) 304 Not Modified ETag: "other-tag" Last-Modified: Sat, 01 Jan 2005 12:01:00 GMT >>> res.last_modified = datetime(2006, 1, 1, 12, 1, tzinfo=UTC) >>> req.get_response(res) >>> res.last_modified = None >>> req.get_response(res) Weak etags:: >>> req = Request.blank('/', if_none_match='W/"test"') >>> res = Response(conditional_response=True, etag='test') >>> req.get_response(res).status '304 Not Modified' Also range response:: >>> res = Response('0123456789', conditional_response=True) >>> req = Request.blank('/', range=(1, 5)) >>> req.range >>> str(req.range) 'bytes=1-4' >>> result = req.get_response(res) >>> result.body '1234' >>> result.content_range.stop 5 >>> result.content_range >>> tuple(result.content_range) (1, 5, 10) >>> result.content_length 4 >>> req.range = (5, 20) >>> str(req.range) 'bytes=5-19' >>> result = req.get_response(res) >>> print result 206 Partial Content Content-Length: 5 Content-Range: bytes 5-9/10 Content-Type: text/html; charset=UTF-8 56789 >>> tuple(result.content_range) (5, 10, 10) >>> req_head = req.copy() >>> req_head.method = 'HEAD' >>> print req_head.get_response(res) 206 Partial Content Content-Length: 5 Content-Range: bytes 5-9/10 Content-Type: text/html; charset=UTF-8 And an invalid requested range: >>> req.range = (10, 20) >>> result = req.get_response(res) >>> print result 416 Requested Range Not Satisfiable Content-Length: 44 Content-Range: bytes */10 Content-Type: text/plain Requested range not satisfiable: bytes=10-19 >>> str(result.content_range) 'bytes */10' >>> req_head = req.copy() >>> req_head.method = 'HEAD' >>> print req_head.get_response(res) 416 Requested Range Not Satisfiable Content-Length: 44 Content-Range: bytes */10 Content-Type: text/plain >>> Request.blank('/', range=(1,2)).get_response( ... Response('0123456789', conditional_response=True)).content_length 1 That was easier; we'll try it with a iterator for the body:: >>> res = Response(conditional_response=True) >>> res.app_iter = ['01234', '567', '89'] >>> req = Request.blank('/') >>> req.range = (1, 5) >>> result = req.get_response(res) Because we don't know the length of the app_iter, this doesn't work:: >>> result.body '0123456789' >>> print result.content_range None But it will, if we set content_length:: >>> res.content_length = 10 >>> req.range = (5, None) >>> result = req.get_response(res) >>> result.body '56789' >>> result.content_range Ranges requesting x last bytes are supported too: >>> req.range = 'bytes=-1' >>> req.range >>> result = req.get_response(res) >>> result.body '9' >>> result.content_range >>> result.content_length 1 If those ranges are not satisfiable, a 416 error is returned: >>> req.range = 'bytes=-100' >>> result = req.get_response(res) >>> result.status '416 Requested Range Not Satisfiable' >>> result.content_range >>> result.body 'Requested range not satisfiable: bytes=-100' If we set Content-Length then we can use it with an app_iter >>> res.content_length = 10 >>> req.range = (1, 5) # python-style range >>> req.range >>> result = req.get_response(res) >>> result.body '1234' >>> result.content_range >>> # And trying If-modified-since >>> res.etag = 'foobar' >>> req.if_range = 'foobar' >>> req.if_range >>> result = req.get_response(res) >>> result.content_range >>> req.if_range = 'blah' >>> result = req.get_response(res) >>> result.content_range >>> req.if_range = datetime(2005, 1, 1, 12, 0, tzinfo=UTC) >>> res.last_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC) >>> result = req.get_response(res) >>> result.content_range >>> res.last_modified = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) >>> result = req.get_response(res) >>> result.content_range Some tests of Content-Range parsing:: >>> from webob.byterange import ContentRange >>> ContentRange.parse('bytes */*') >>> ContentRange.parse('bytes */10') >>> ContentRange.parse('bytes 5-9/10') >>> ContentRange.parse('bytes 5-10/*') >>> print ContentRange.parse('bytes 5-10/10') None >>> print ContentRange.parse('bytes 5-4/10') None >>> print ContentRange.parse('bytes 5-*/10') None Some tests of exceptions:: >>> from webob import exc >>> res = exc.HTTPNotFound('Not found!') >>> res.content_type = 'text/plain' >>> res.content_type 'text/plain' >>> res = exc.HTTPNotModified() >>> res.headers ResponseHeaders([]) Headers can be set to unicode values:: >>> res = Response('test') >>> res.etag = u'fran\xe7ais' But they come out as str:: >>> res.etag 'fran\xe7ais' Unicode can come up in unexpected places, make sure it doesn't break things (this particular case could be caused by a `from __future__ import unicode_literals`):: >>> Request.blank('/', method=u'POST').get_response(exc.HTTPMethodNotAllowed()) Copying Responses should copy their internal structures >>> r = Response(app_iter=[]) >>> r2 = r.copy() >>> r.headerlist is r2.headerlist False >>> r.app_iter is r2.app_iter False >>> r = Response(app_iter=iter(['foo'])) >>> r2 = r.copy() >>> del r2.content_type >>> r2.body_file.write(' bar') >>> print r 200 OK Content-Type: text/html; charset=UTF-8 foo >>> print r2 200 OK Content-Length: 7 foo bar Additional Response constructor keywords are used to set attributes >>> r = Response(cache_expires=True) >>> r.headers['Cache-Control'] 'max-age=0, must-revalidate, no-cache, no-store' >>> from webob.exc import HTTPBadRequest >>> raise HTTPBadRequest('bad data') Traceback (most recent call last): ... HTTPBadRequest: bad data >>> raise HTTPBadRequest() Traceback (most recent call last): ... HTTPBadRequest: The server could not comply with the request since it is either malformed or otherwise incorrect. WebOb-1.3.1/docs/differences.txt0000664000175000017500000004576412201331167017272 0ustar chrismchrism00000000000000Differences Between WebOb and Other Systems +++++++++++++++++++++++++++++++++++++++++++ This document points out some of the API differences between the Request and Response object, and the objects in other systems. .. contents:: paste.wsgiwrappers and Pylons ============================= The Pylons ``request`` and ``response`` object are based on ``paste.wsgiwrappers.WSGIRequest`` and ``WSGIResponse`` There is no concept of ``defaults`` in WebOb. In Paste/Pylons these serve as threadlocal settings that control certain policies on the request and response object. In WebOb you should make your own subclasses to control policy (though in many ways simply being explicit elsewhere removes the need for this policy). Request ------- ``body``: This is a file-like object in WSGIRequest. In WebOb it is a string (to match Response.body) and the file-like object is available through ``req.body_file`` ``languages()``: This is available through ``req.accept_language``, particularly ``req.accept_language.best_matches(fallback_language)`` ``match_accept(mimetypes)``: This is available through ``req.accept.first_match(mimetypes)``; or if you trust the client's quality ratings, you can use ``req.accept.best_match(mimetypes)`` ``errors``: This controls how unicode decode errors are handled; it is now named ``unicode_errors`` There are also many extra methods and attributes on WebOb Request objects. Response -------- ``determine_charset()``: Is now available as ``res.charset`` ``has_header(header)``: Should be done with ``header in res.headers`` ``get_content()`` and ``wsgi_response()``: These are gone; you should use ``res.body`` or ``res(environ, start_response)`` ``write(content)``: Available in ``res.body_file.write(content)``. ``flush()`` and ``tell()``: Not available. There are also many extra methods and attributes on WebOb Response objects. Django ====== This is a quick summary from reading `the Django documentation `_. Request ------- ``encoding``: Is ``req.charset`` ``REQUEST``: Is ``req.params`` ``FILES``: File uploads are ``cgi.FieldStorage`` objects directly in ``res.POST`` ``META``: Is ``req.environ`` ``user``: No equivalent (too connected to application model for WebOb). There is ``req.remote_user``, which is only ever a string. ``session``: No equivalent ``raw_post_data``: Available with ``req.body`` ``__getitem__(key)``: You have to use ``req.params`` ``is_secure()``: No equivalent; you could use ``req.scheme == 'https'``. QueryDict --------- QueryDict is the way Django represents the multi-key dictionary-like objects that are request variables (query string and POST body variables). The equivalent in WebOb is MultiDict. Mutability: WebOb dictionaries are sometimes mutable (req.GET is, req.params is not) Ordering: I believe Django does not order the keys fully; MultiDict is a full ordering. Methods that iterate over the parameters iterate over keys in their order in the original request. ``keys()``, ``items()``, ``values()`` (plus ``iter*``): These return all values in MultiDict, but only the last value for a QueryDict. That is, given ``a=1&a=2`` with MultiDict ``d.items()`` returns ``[('a', '1'), ('a', '2')]``, but QueryDict returns ``[('a', '1')]`` ``getlist(key)``: Available as ``d.getall(key)`` ``setlist(key)``: No direct equivalent ``appendlist(key, value)``: Available as ``d.add(key, value)`` ``setlistdefault(key, default_list)``: No direct equivalent ``lists()``: Is ``d.dict_of_lists()`` The MultiDict object has a ``d.getone(key)`` method, that raises KeyError if there is not exactly one key. There is a method ``d.mixed()`` which returns a version where values are lists *if* there are multiple values for a list. This is similar to how many cgi-based request forms are represented. Response -------- Constructor: Somewhat different. WebOb takes any keyword arguments as attribute assignments. Django only takes a couple arguments. The ``mimetype`` argument is ``content_type``, and ``content_type`` is the entire ``Content-Type`` header (including charset). dictionary-like: The Django response object is somewhat dictionary-like, setting headers. The equivalent dictionary-like object is ``res.headers``. In WebOb this is a MultiDict. ``has_header(header)``: Use ``header in res.headers`` ``flush()``, ``tell()``: Not available ``content``: Use ``res.body`` for the ``str`` value, ``res.unicode_body`` for the ``unicode`` value Response Subclasses ------------------- These are generally like ``webob.exc`` objects. ``HttpResponseNotModified`` is ``HTTPNotModified``; this naming translation generally works. CherryPy/TurboGears =================== The `CherryPy request object `_ is also used by TurboGears 1.x. Request ------- ``app``: No equivalent ``base``: ``req.application_url`` ``close()``: No equivalent ``closed``: No equivalent ``config``: No equivalent ``cookie``: A ``SimpleCookie`` object in CherryPy; a dictionary in WebOb (``SimpleCookie`` can represent cookie parameters, but cookie parameters are only sent with responses not requests) ``dispatch``: No equivalent (this is the object dispatcher in CherryPy). ``error_page``, ``error_response``, ``handle_error``: No equivalent ``get_resource()``: Similar to ``req.get_response(app)`` ``handler``: No equivalent ``headers``, ``header_list``: The WSGI environment represents headers as a dictionary, available through ``req.headers`` (no list form is available in the request). ``hooks``: No equivalent ``local``: No equivalent ``methods_with_bodies``: This represents methods where CherryPy will automatically try to read the request body. WebOb lazily reads POST requests with the correct content type, and no other bodies. ``namespaces``: No equivalent ``protocol``: As ``req.environ['SERVER_PROTOCOL']`` ``query_string``: As ``req.query_string`` ``remote``: ``remote.ip`` is like ``req.remote_addr``. ``remote.port`` is not available. ``remote.name`` is in ``req.environ.get('REMOTE_HOST')`` ``request_line``: No equivalent ``respond()``: A method that is somewhat similar to ``req.get_response()``. ``rfile``: ``req.body_file`` ``run``: No equivalent ``server_protocol``: As ``req.environ['SERVER_PROTOCOL']`` ``show_tracebacks``: No equivalent ``throw_errors``: No equivalent ``throws``: No equivalent ``toolmaps``: No equivalent ``wsgi_environ``: As ``req.environ`` Response -------- From information `from the wiki `_. ``body``: This is an iterable in CherryPy, a string in WebOb; ``res.app_iter`` gives an iterable in WebOb. ``check_timeout``: No equivalent ``collapse_body()``: This turns a stream/iterator body into a single string. Accessing ``res.body`` will do this automatically. ``cookie``: Accessible through ``res.set_cookie(...)``, ``res.delete_cookie``, ``res.unset_cookie()`` ``finalize()``: No equivalent ``header_list``: In ``res.headerlist`` ``stream``: This can make CherryPy stream the response body out directory. There is direct no equivalent; you can use a dynamically generated iterator to do something similar. ``time``: No equivalent ``timed_out``: No equivalent Yaro ==== `Yaro `_ is a small wrapper around the WSGI environment, much like WebOb in scope. The WebOb objects have many more methods and attributes. The Yaro Response object is a much smaller subset of WebOb's Response. Request ------- ``query``: As ``req.GET`` ``form``: As ``req.POST`` ``cookie``: A ``SimpleCookie`` object in Yaro; a dictionary in WebOb (``SimpleCookie`` can represent cookie parameters, but cookie parameters are only sent with responses not requests) ``uri``: Returns a URI object, no equivalent (only string URIs available). ``redirect``: Not available (response-related). ``webob.exc.HTTPFound()`` can be useful here. ``forward(yaroapp)``, ``wsgi_forward(wsgiapp)``: Available with ``req.get_response(app)`` and ``req.call_application(app)``. In both cases it is a WSGI application in WebOb, there is no special kind of communication; ``req.call_application()`` just returns a ``webob.Response`` object. ``res``: The request object in WebOb *may* have a ``req.response`` attribute. Werkzeug ======== An offshoot of `Pocoo `_, this library is based around WSGI, similar to Paste and Yaro. This is taken from the `wrapper documentation `_. Request ------- path: As ``req.path_info`` args: As ``req.GET`` form: As ``req.POST`` values: As ``req.params`` files: In ``req.POST`` (as FieldStorage objects) data: In ``req.body_file`` Response -------- response: In ``res.body`` (settable as ``res.body`` or ``res.app_iter``) status: In ``res.status_code`` mimetype: In ``res.content_type`` Zope 3 ====== From the Zope 3 interfaces for the `Request `_ and `Response `_. Request ------- ``locale``, ``setupLocale()``: This is not fully calculated, but information is available in ``req.accept_languages``. ``principal``, ``setPrincipal(principal)``: ``req.remote_user`` gives the username, but there is no standard place for a user *object*. ``publication``, ``setPublication()``, These are associated with the object publishing system in Zope. This kind of publishing system is outside the scope of WebOb. ``traverse(object)``, ``getTraversalStack()``, ``setTraversalStack()``: These all relate to traversal, which is part of the publishing system. ``processInputs()``, ``setPathSuffix(steps)``: Also associated with traversal and preparing the request. ``environment``: In ``req.environ`` ``bodyStream``: In ``req.body_file`` ``interaction``: This is the security context for the request; all the possible participants or principals in the request. There's no equivalent. ``annotations``: Extra information associated with the request. This would generally go in custom keys of ``req.environ``, or if you set attributes those attributes are stored in ``req.environ['webob.adhoc_attrs']``. ``debug``: There is no standard debug flag for WebOb. ``__getitem__(key)``, ``get(key)``, etc: These treat the request like a dictionary, which WebOb does not do. They seem to take values from the environment, not parameters. Also on the Zope request object is ``items()``, ``__contains__(key)``, ``__iter__()``, ``keys()``, ``__len__()``, ``values()``. ``getPositionalArguments()``: I'm not sure what the equivalent would be, as there are no positional arguments during instantiation (it doesn't fit into WSGI). Maybe ``wsgiorg.urlvars``? ``retry()``, ``supportsRetry()``: Creates a new request that can be used to retry a request. Similar to ``req.copy()``. ``close()``, ``hold(obj)``: This closes resources associated with the request, including any "held" objects. There's nothing similar. Response -------- ``authUser``: Not sure what this is or does. ``reset()``: No direct equivalent; you'd have to do ``res.headers = []; res.body = ''; res.status = 200`` ``setCookie(name, value, **kw)``: Is ``res.set_cookie(...)``. ``getCookie(name)``: No equivalent. Hm. ``expireCookie(name)``: Is ``res.delete_cookie(name)``. ``appendToCookie(name, value)``: This appends the value to any existing cookie (separating values with a colon). WebOb does not do this. ``setStatus(status)``: Availble by setting ``res.status`` (can be set to an integer or a string of "code reason"). ``getHeader(name, default=None)``: Is ``res.headers.get(name)``. ``getStatus()``: Is ``res.status_code`` (or ``res.status`` to include reason) ``addHeader(name, value)``: Is ``res.headers.add(name, value)`` (in Zope and WebOb, this does not clobber any previous value). ``getHeaders()``: Is ``res.headerlist``. ``setHeader(name, value)``: Is ``res.headers[name] = value``. ``getStatusString()``: Is ``res.status``. ``consumeBody()``: This consumes any non-string body to turn the body into a single string. Any access to ``res.body`` will do this (e.g., when you have set the ``res.app_iter``). ``internalError()``: This is available with ``webob.exc.HTTP*()``. ``handleException(exc_info)``: This is provided with a tool like ``paste.exceptions``. ``consumeBodyIter()``: This returns the iterable for the body, even if the body was a string. Anytime you access ``res.app_iter`` you will get an iterable. ``res.body`` and ``res.app_iter`` can be interchanged and accessed as many times as you want, unlike the Zope equivalents. ``setResult(result)``: You can achieve the same thing through ``res.body = result``, or ``res.app_iter = result``. ``res.body`` accepts None, a unicode string (*if* you have set a charset) or a normal string. ``res.app_iter`` only accepts None and an interable. You can't update all of a response with one call. Like in Zope, WebOb updates Content-Length. Unlike Zope, it does not automatically calculate a charset. mod_python ========== Some key attributes from the `mod_python `_ request object. Request ------- ``req.uri``: In ``req.path``. ``req.user``: In ``req.remote_user``. ``req.get_remote_host()``: In ``req.environ['REMOTE_ADDR']`` or ``req.remote_addr``. ``req.headers_in.get('referer')``: In ``req.headers.get('referer')`` or ``req.referer`` (same pattern for other request headers, presumably). Response -------- ``util.redirect`` or ``req.status = apache.HTTP_MOVED_TEMPORARILY``: .. code-block:: python from webob.exc import HTTPTemporaryRedirect exc = HTTPTemporaryRedirect(location=url) return exc(environ, start_response) ``req.content_type = "application/x-csv"`` and ``req.headers_out.add('Content-Disposition', 'attachment;filename=somefile.csv')``: .. code-block:: python res = req.ResponseClass() res.content_type = 'application/x-csv' res.headers.add('Content-Disposition', 'attachment;filename=somefile.csv') return res(environ, start_response) webapp Response =============== The Google App Engine `webapp `_ framework uses the WebOb Request object, but does not use its Response object. The constructor for ``webapp.Response`` does not take any arguments. The response is created by the framework, so you don't use it like ``return Response(...)``, instead you use ``self.response``. Also the response object automatically has ``Cache-Control: no-cache`` set, while the WebOb response does not set any cache headers. ``resp.set_status(code, message=None)``: This is handled by setting the ``resp.status`` attribute. ``resp.clear()``: You'd do ``resp.body = ""`` ``resp.wsgi_write(start_response)``: This writes the response using the ``start_response`` callback, and using the ``start_response`` writer. The WebOb response object is called as a WSGI app (``resp(environ, start_response)``) to do the equivalent. ``resp.out.write(text)``: This writes to an internal ``StringIO`` instance of the response. This uses the ability of the standard StringIO object to hold either unicode or ``str`` text, and so long as you are always consistent it will encode your content (but it does not respect your preferred encoding, it always uses UTF-8). The WebOb method ``resp.write(text)`` is basically equivalent, and also accepts unicode (using ``resp.charset`` for the encoding). You can also write to ``resp.body_file``, but it does not allow unicode. Besides exposing a ``.headers`` attribute (based on `wsgiref.headers.Headers `_) there is no other API for the webapp response object. This means the response lacks: * A usefully readable body or status. * A useful constructor that makes it easy to treat responses like objects. * Providing a non-string ``app_iter`` for the body (like a generator). * Parsing of the Content-Type charset. * Getter/setters for parsed forms of headers, specifically cache_control and last_modified. * The ``cache_expires`` method * ``set_cookie``, ``delete_cookie``, and ``unset_cookie``. Instead you have to simply manually set the Set-Cookie header. * ``encode_content`` and ``decode_content`` for handling gzip encoding. * ``md5_etag()`` for generating an etag from the body. * Conditional responses that will return 304 based on the response and request headers. * The ability to serve Range request automatically. PHP === PHP does not have anything really resembling a request and response object. Instead these are encoded in a set of global objects for the request and functions for the response. ``$_POST``, ``$_GET``, ``$_FILES`` ---------------------------------- These represent ``req.POST`` and ``req.GET``. PHP uses the variable names to tell whether a variable can hold multiple values. For instance ``$_POST['name[]']``, which will be an array. In WebOb any variable can have multiple values, and you can get these through ``req.POST.getall('name')``. The files in ``$_FILES`` are simply in ``req.POST`` in WebOb, as FieldStorage instances. ``$_COOKIES`` ------------- This is in ``req.cookies``. ``$_SERVER``, ``$_REQUEST``, ``$_ENV`` -------------------------------------- These are all in ``req.environ``. These are not split up like they are in PHP, it's all just one dictionary. Everything that would typically be in ``$_ENV`` is technically optional, and outside of a couple CGI-standard keys in ``$_SERVER`` most of those are also optional, but it is common for WSGI servers to populate the request with similar information as PHP. ``$HTTP_RAW_POST_DATA`` ----------------------- This contains the unparsed data in the request body. This is in ``req.body``. The response ------------ Response headers in PHP are sent with ``header("Header-Name: value")``. In WebOb there is a dictionary in ``resp.headers`` that can have values set; the headers aren't actually sent until you send the response. You can add headers without overwriting (the equivalent of ``header("...", false)``) with ``resp.headers.add('Header-Name', 'value')``. The status in PHP is sent with ``http_send_status(code)``. In WebOb this is ``resp.status = code``. The body in PHP is sent implicitly through the rendering of the PHP body (or with ``echo`` or any other functions that send output). WebOb-1.3.1/docs/wiki-example-code/0000775000175000017500000000000012252637046017553 5ustar chrismchrism00000000000000WebOb-1.3.1/docs/wiki-example-code/example.py0000664000175000017500000001331212201331167021545 0ustar chrismchrism00000000000000import os import re from webob import Request, Response from webob import exc from tempita import HTMLTemplate VIEW_TEMPLATE = HTMLTemplate("""\ {{page.title}}

{{page.title}}

{{if message}}
{{message}}
{{endif}}
{{page.content|html}}

Edit """) EDIT_TEMPLATE = HTMLTemplate("""\ Edit: {{page.title}} {{if page.exists}}

Edit: {{page.title}}

{{else}}

Create: {{page.title}}

{{endif}}
Title:
Content: Cancel

Cancel
""") class WikiApp(object): view_template = VIEW_TEMPLATE edit_template = EDIT_TEMPLATE def __init__(self, storage_dir): self.storage_dir = os.path.abspath(os.path.normpath(storage_dir)) def __call__(self, environ, start_response): req = Request(environ) action = req.params.get('action', 'view') page = self.get_page(req.path_info) try: try: meth = getattr(self, 'action_%s_%s' % (action, req.method)) except AttributeError: raise exc.HTTPBadRequest('No such action %r' % action) resp = meth(req, page) except exc.HTTPException, e: resp = e return resp(environ, start_response) def get_page(self, path): path = path.lstrip('/') if not path: path = 'index' path = os.path.join(self.storage_dir, path) path = os.path.normpath(path) if path.endswith('/'): path += 'index' if not path.startswith(self.storage_dir): raise exc.HTTPBadRequest("Bad path") path += '.html' return Page(path) def action_view_GET(self, req, page): if not page.exists: return exc.HTTPTemporaryRedirect( location=req.url + '?action=edit') if req.cookies.get('message'): message = req.cookies['message'] else: message = None text = self.view_template.substitute( page=page, req=req, message=message) resp = Response(text) if message: resp.delete_cookie('message') else: resp.last_modified = page.mtime resp.conditional_response = True return resp def action_view_POST(self, req, page): submit_mtime = int(req.params.get('mtime') or '0') or None if page.mtime != submit_mtime: return exc.HTTPPreconditionFailed( "The page has been updated since you started editing it") page.set( title=req.params['title'], content=req.params['content']) resp = exc.HTTPSeeOther( location=req.path_url) resp.set_cookie('message', 'Page updated') return resp def action_edit_GET(self, req, page): text = self.edit_template.substitute( page=page, req=req) return Response(text) class Page(object): def __init__(self, filename): self.filename = filename @property def exists(self): return os.path.exists(self.filename) @property def title(self): if not self.exists: # we need to guess the title basename = os.path.splitext(os.path.basename(self.filename))[0] basename = re.sub(r'[_-]', ' ', basename) return basename.capitalize() content = self.full_content match = re.search(r'(.*?)', content, re.I|re.S) return match.group(1) @property def full_content(self): f = open(self.filename, 'rb') try: return f.read() finally: f.close() @property def content(self): if not self.exists: return '' content = self.full_content match = re.search(r']*>(.*?)', content, re.I|re.S) return match.group(1) @property def mtime(self): if not self.exists: return None else: return int(os.stat(self.filename).st_mtime) def set(self, title, content): dir = os.path.dirname(self.filename) if not os.path.exists(dir): os.makedirs(dir) new_content = """%s%s""" % ( title, content) f = open(self.filename, 'wb') f.write(new_content) f.close() if __name__ == '__main__': import optparse parser = optparse.OptionParser( usage='%prog --port=PORT' ) parser.add_option( '-p', '--port', default='8080', dest='port', type='int', help='Port to serve on (default 8080)') parser.add_option( '--wiki-data', default='./wiki', dest='wiki_data', help='Place to put wiki data into (default ./wiki/)') options, args = parser.parse_args() print 'Writing wiki pages to %s' % options.wiki_data app = WikiApp(options.wiki_data) from wsgiref.simple_server import make_server httpd = make_server('localhost', options.port, app) print 'Serving on http://localhost:%s' % options.port try: httpd.serve_forever() except KeyboardInterrupt: print '^C' WebOb-1.3.1/docs/comment-example-code/0000775000175000017500000000000012252637046020252 5ustar chrismchrism00000000000000WebOb-1.3.1/docs/comment-example-code/example.py0000644000175000017500000001174711743245714022270 0ustar chrismchrism00000000000000import os import urllib import time import re from cPickle import load, dump from webob import Request, Response, html_escape from webob import exc class Commenter(object): def __init__(self, app, storage_dir): self.app = app self.storage_dir = storage_dir if not os.path.exists(storage_dir): os.makedirs(storage_dir) def __call__(self, environ, start_response): req = Request(environ) if req.path_info_peek() == '.comments': return self.process_comment(req)(environ, start_response) # This is the base path of *this* middleware: base_url = req.application_url resp = req.get_response(self.app) if resp.content_type != 'text/html' or resp.status_code != 200: # Not an HTML response, we don't want to # do anything to it return resp(environ, start_response) # Make sure the content isn't gzipped: resp.decode_content() comments = self.get_data(req.url) body = resp.body body = self.add_to_end(body, self.format_comments(comments)) body = self.add_to_end(body, self.submit_form(base_url, req)) resp.body = body return resp(environ, start_response) def get_data(self, url): # Double-quoting makes the filename safe filename = self.url_filename(url) if not os.path.exists(filename): return [] else: f = open(filename, 'rb') data = load(f) f.close() return data def save_data(self, url, data): filename = self.url_filename(url) f = open(filename, 'wb') dump(data, f) f.close() def url_filename(self, url): return os.path.join(self.storage_dir, urllib.quote(url, '')) _end_body_re = re.compile(r'', re.I|re.S) def add_to_end(self, html, extra_html): """ Adds extra_html to the end of the html page (before ) """ match = self._end_body_re.search(html) if not match: return html + extra_html else: return html[:match.start()] + extra_html + html[match.start():] def format_comments(self, comments): if not comments: return '' text = [] text.append('
') text.append('

Comments (%s):

' % len(comments)) for comment in comments: text.append('

%s at %s:

' % ( html_escape(comment['homepage']), html_escape(comment['name']), time.strftime('%c', comment['time']))) # Susceptible to XSS attacks!: text.append(comment['comments']) return ''.join(text) def submit_form(self, base_path, req): return '''

Leave a comment:

Name:
URL:
Comments:

''' % (base_path, html_escape(req.url)) def process_comment(self, req): try: url = req.params['url'] name = req.params['name'] homepage = req.params['homepage'] comments = req.params['comments'] except KeyError, e: resp = exc.HTTPBadRequest('Missing parameter: %s' % e) return resp data = self.get_data(url) data.append(dict( name=name, homepage=homepage, comments=comments, time=time.gmtime())) self.save_data(url, data) resp = exc.HTTPSeeOther(location=url+'#comment-area') return resp if __name__ == '__main__': import optparse parser = optparse.OptionParser( usage='%prog --port=PORT BASE_DIRECTORY' ) parser.add_option( '-p', '--port', default='8080', dest='port', type='int', help='Port to serve on (default 8080)') parser.add_option( '--comment-data', default='./comments', dest='comment_data', help='Place to put comment data into (default ./comments/)') options, args = parser.parse_args() if not args: parser.error('You must give a BASE_DIRECTORY') base_dir = args[0] from paste.urlparser import StaticURLParser app = StaticURLParser(base_dir) app = Commenter(app, options.comment_data) from wsgiref.simple_server import make_server httpd = make_server('localhost', options.port, app) print 'Serving on http://localhost:%s' % options.port try: httpd.serve_forever() except KeyboardInterrupt: print '^C' WebOb-1.3.1/docs/license.txt0000644000175000017500000000210111636566135016427 0ustar chrismchrism00000000000000License ======= Copyright (c) 2007 Ian Bicking and Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. WebOb-1.3.1/setup.cfg0000664000175000017500000000043112252637046015136 0ustar chrismchrism00000000000000[aliases] distribute = register sdist upload dev = develop easy_install webob[testing] docs = develop easy_install webob[docs] [nosetests] detailed-errors = True cover-erase = True cover-package = webob nocapture = True [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 WebOb-1.3.1/setup.py0000664000175000017500000000365412252634127015036 0ustar chrismchrism00000000000000from setuptools import setup version = '1.3.1' testing_extras = ['nose', 'coverage'] docs_extras = ['Sphinx'] setup( name='WebOb', version=version, description="WSGI request and response object", long_description="""\ WebOb provides wrappers around the WSGI request environment, and an object to help create WSGI responses. The objects map much of the specified behavior of HTTP, including header parsing and accessors for other standard parts of the environment. You may install the `in-development version of WebOb `_ with ``pip install WebOb==dev`` (or ``easy_install WebOb==dev``). * `WebOb reference `_ * `Bug tracker `_ * `Browse source code `_ * `Mailing list `_ * `Release news `_ * `Detailed changelog `_ """, classifiers=[ "Development Status :: 6 - Mature", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Topic :: Internet :: WWW/HTTP :: WSGI", "Topic :: Internet :: WWW/HTTP :: WSGI :: Application", "Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware", "Programming Language :: Python :: 2.6", "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3.2", "Programming Language :: Python :: 3.3", ], keywords='wsgi request web http', author='Ian Bicking', author_email='ianb@colorstudy.com', maintainer='Pylons Project', url='http://webob.org/', license='MIT', packages=['webob'], zip_safe=True, test_suite='nose.collector', tests_require=['nose'], extras_require = { 'testing':testing_extras, 'docs':docs_extras, }, ) WebOb-1.3.1/tox.ini0000664000175000017500000000044012123263150014614 0ustar chrismchrism00000000000000[tox] envlist = py26,py27,py32,py33,pypy,cover [testenv] commands = python setup.py dev python setup.py nosetests [testenv:cover] basepython = python2.6 commands = python setup.py dev python setup.py nosetests --with-xunit --with-xcoverage deps = nosexcover WebOb-1.3.1/MANIFEST.in0000644000175000017500000000011511636566135015055 0ustar chrismchrism00000000000000recursive-include tests * recursive-include docs * prune *.pyc prune *.pyoWebOb-1.3.1/tests/0000775000175000017500000000000012252637046014461 5ustar chrismchrism00000000000000WebOb-1.3.1/tests/performance_test.py0000644000175000017500000000315011645621753020373 0ustar chrismchrism00000000000000#!/usr/bin/env python from webob.response import Response def make_middleware(app): from repoze.profile.profiler import AccumulatingProfileMiddleware return AccumulatingProfileMiddleware( app, log_filename='/tmp/profile.log', discard_first_request=True, flush_at_shutdown=True, path='/__profile__') def simple_app(environ, start_response): resp = Response('Hello world!') return resp(environ, start_response) if __name__ == '__main__': import sys import os import signal if sys.argv[1:]: arg = sys.argv[1] else: arg = None if arg in ['open', 'run']: import subprocess import webbrowser import time os.environ['SHOW_OUTPUT'] = '0' proc = subprocess.Popen([sys.executable, __file__]) time.sleep(1) subprocess.call(['ab', '-n', '1000', 'http://localhost:8080/']) if arg == 'open': webbrowser.open('http://localhost:8080/__profile__') print('Hit ^C to end') try: while 1: raw_input() finally: os.kill(proc.pid, signal.SIGKILL) else: from paste.httpserver import serve if os.environ.get('SHOW_OUTPUT') != '0': print('Note you can also use:)') print(' %s %s open' % (sys.executable, __file__)) print('to run ab and open a browser (or "run" to just run ab)') print('Now do:') print('ab -n 1000 http://localhost:8080/') print('wget -O - http://localhost:8080/__profile__') serve(make_middleware(simple_app)) WebOb-1.3.1/tests/test_transcode.py0000644000175000017500000000521511645621753020060 0ustar chrismchrism00000000000000# coding: cp1251 from webob.request import Request, Transcoder from webob.response import Response from webob.compat import text_, native_ from nose.tools import eq_ # def tapp(env, sr): # req = Request(env) # r = Response(str(req)) # #r = Response(str(dict(req.POST))) # return r(env, sr) t1 = b'--BOUNDARY\r\nContent-Disposition: form-data; name="a"\r\n\r\n\xea\xf3...\r\n--BOUNDARY--' t2 = b'--BOUNDARY\r\nContent-Disposition: form-data; name="a"; filename="file"\r\n\r\n\xea\xf3...\r\n--BOUNDARY--' t3 = b'--BOUNDARY\r\nContent-Disposition: form-data; name="a"; filename="\xea\xf3..."\r\n\r\nfoo\r\n--BOUNDARY--' def test_transcode(): def tapp(env, sr): req = Request(env) #import pprint; pprint.pprint(req.environ) #print(req.body) req = req.decode() #import pprint; pprint.pprint(req.environ) #print(req.body) v = req.POST[req.query_string] if hasattr(v, 'filename'): r = Response(text_('%s\n%r' % (v.filename, v.value))) else: r = Response(v) return r(env, sr) text = b'\xea\xf3...'.decode('cp1251') def test(post): req = Request.blank('/?a', POST=post) req.environ['CONTENT_TYPE'] = 'multipart/form-data; charset=windows-1251; boundary=BOUNDARY' return req.get_response(tapp) r = test(t1) eq_(r.text, text) r = test(t2) eq_(r.text, 'file\n%r' % text.encode('cp1251')) r = test(t3) eq_(r.text, "%s\n%r" % (text, b'foo')) #req = Request.blank('/?a', POST={'a': ('file', text.encode('cp1251'))}, # req = Request({}, charset='utf8') # req = Request({}) # print req.charset # print req._charset_cache # print req.environ.get('CONTENT_TYPE') #print '\xd0\xba\xd1\x83...'.decode('utf8').encode('cp1251') #print u'\u043a'.encode('cp1251') def test_transcode_query(): req = Request.blank('/?%EF%F0%E8=%E2%E5%F2') req2 = req.decode('cp1251') eq_(req2.query_string, '%D0%BF%D1%80%D0%B8=%D0%B2%D0%B5%D1%82') def test_transcode_non_multipart(): req = Request.blank('/?a', POST='%EF%F0%E8=%E2%E5%F2') req._content_type_raw = 'application/x-www-form-urlencoded' req2 = req.decode('cp1251') eq_(native_(req2.body), '%D0%BF%D1%80%D0%B8=%D0%B2%D0%B5%D1%82') def test_transcode_non_form(): req = Request.blank('/?a', POST='%EF%F0%E8=%E2%E5%F2') req._content_type_raw = 'application/x-foo' req2 = req.decode('cp1251') eq_(native_(req2.body), '%EF%F0%E8=%E2%E5%F2') def test_transcode_noop(): req = Request.blank('/') assert req.decode() is req def test_transcode_query(): t = Transcoder('ascii') eq_(t.transcode_query('a'), 'a') WebOb-1.3.1/tests/test_client_functional.py0000664000175000017500000000633611752331630021574 0ustar chrismchrism00000000000000import time import urllib from webob import Request, Response from webob.dec import wsgify from webob.client import SendRequest from .test_in_wsgiref import serve from nose.tools import assert_raises @wsgify def simple_app(req): data = {'headers': dict(req.headers), 'body': req.text, 'method': req.method, } return Response(json=data) def test_client(client_app=None): with serve(simple_app) as server: req = Request.blank(server.url, method='POST', content_type='application/json', json={'test': 1}) resp = req.send(client_app) assert resp.status_code == 200, resp.status assert resp.json['headers']['Content-Type'] == 'application/json' assert resp.json['method'] == 'POST' # Test that these values get filled in: del req.environ['SERVER_NAME'] del req.environ['SERVER_PORT'] resp = req.send(client_app) assert resp.status_code == 200, resp.status req = Request.blank(server.url) del req.environ['SERVER_NAME'] del req.environ['SERVER_PORT'] assert req.send(client_app).status_code == 200 req.headers['Host'] = server.url.lstrip('http://') del req.environ['SERVER_NAME'] del req.environ['SERVER_PORT'] resp = req.send(client_app) assert resp.status_code == 200, resp.status del req.environ['SERVER_NAME'] del req.environ['SERVER_PORT'] del req.headers['Host'] assert req.environ.get('SERVER_NAME') is None assert req.environ.get('SERVER_PORT') is None assert req.environ.get('HTTP_HOST') is None assert_raises(ValueError, req.send, client_app) req = Request.blank(server.url) req.environ['CONTENT_LENGTH'] = 'not a number' assert req.send(client_app).status_code == 200 def no_length_app(environ, start_response): start_response('200 OK', [('Content-type', 'text/plain')]) return [b'ok'] def test_no_content_length(client_app=None): with serve(no_length_app) as server: req = Request.blank(server.url) resp = req.send(client_app) assert resp.status_code == 200, resp.status @wsgify def cookie_app(req): resp = Response('test') resp.headers.add('Set-Cookie', 'a=b') resp.headers.add('Set-Cookie', 'c=d') resp.headerlist.append(('X-Crazy', 'value\r\n continuation')) return resp def test_client_cookies(client_app=None): with serve(cookie_app) as server: req = Request.blank(server.url + '/?test') resp = req.send(client_app) assert resp.headers.getall('Set-Cookie') == ['a=b', 'c=d'] assert resp.headers['X-Crazy'] == 'value, continuation', repr(resp.headers['X-Crazy']) @wsgify def slow_app(req): time.sleep(2) return Response('ok') def test_client_slow(client_app=None): if client_app is None: client_app = SendRequest() if not client_app._timeout_supported(client_app.HTTPConnection): # timeout isn't supported return with serve(slow_app) as server: req = Request.blank(server.url) req.environ['webob.client.timeout'] = 0.1 resp = req.send(client_app) assert resp.status_code == 504, resp.status WebOb-1.3.1/tests/test_misc.py0000664000175000017500000000715212123261311017013 0ustar chrismchrism00000000000000import cgi from webob.util import html_escape from webob.multidict import MultiDict from nose.tools import eq_ as eq, assert_raises from webob.compat import ( text_, PY3 ) def test_html_escape(): if PY3: EXPECTED_LT = 'expected a '<'.' else: EXPECTED_LT = "expected a '<'." for v, s in [ # unsafe chars ('these chars: < > & "', 'these chars: < > & "'), (' ', ' '), ('è', '&egrave;'), # The apostrophe is *not* escaped, which some might consider to be # a serious bug (see, e.g. http://www.cvedetails.com/cve/CVE-2010-2480/) (text_('the majestic m\xf8ose'), 'the majestic møose'), #("'", "'") # 8-bit strings are passed through (text_('\xe9'), 'é'), ## (text_(b'the majestic m\xf8ose').encode('utf-8'), ## 'the majestic m\xc3\xb8ose'), # ``None`` is treated specially, and returns the empty string. (None, ''), # Objects that define a ``__html__`` method handle their own escaping (t_esc_HTML(), '
hello
'), # Things that are not strings are converted to strings and then escaped (42, '42'), (Exception("expected a '<'."), EXPECTED_LT), # If an object implements both ``__str__`` and ``__unicode__``, the latter # is preferred (t_esc_SuperMoose(), 'møose'), (t_esc_Unicode(), 'é'), (t_esc_UnsafeAttrs(), '<UnsafeAttrs>'), ]: eq(html_escape(v), s) class t_esc_HTML(object): def __html__(self): return '
hello
' class t_esc_Unicode(object): def __unicode__(self): return text_(b'\xe9') class t_esc_UnsafeAttrs(object): attr = 'value' def __getattr__(self, k): return self.attr def __repr__(self): return '' class t_esc_SuperMoose(object): def __str__(self): return text_(b'm\xf8ose').encode('utf-8') def __unicode__(self): return text_(b'm\xf8ose') def test_multidict(): d = MultiDict(a=1, b=2) eq(d['a'], 1) eq(d.getall('c'), []) d.add('a', 2) eq(d['a'], 2) eq(d.getall('a'), [1, 2]) d['b'] = 4 eq(d.getall('b'), [4]) eq(list(d.keys()), ['a', 'a', 'b']) eq(list(d.items()), [('a', 1), ('a', 2), ('b', 4)]) eq(d.mixed(), {'a': [1, 2], 'b': 4}) # test getone # KeyError: "Multiple values match 'a': [1, 2]" assert_raises(KeyError, d.getone, 'a') eq(d.getone('b'), 4) # KeyError: "Key not found: 'g'" assert_raises(KeyError, d.getone, 'g') eq(d.dict_of_lists(), {'a': [1, 2], 'b': [4]}) assert 'b' in d assert 'e' not in d d.clear() assert 'b' not in d d['a'] = 4 d.add('a', 5) e = d.copy() assert 'a' in e e.clear() e['f'] = 42 d.update(e) eq(d, MultiDict([('a', 4), ('a', 5), ('f', 42)])) f = d.pop('a') eq(f, 4) eq(d['a'], 5) eq(d.pop('g', 42), 42) assert_raises(KeyError, d.pop, 'n') # TypeError: pop expected at most 2 arguments, got 3 assert_raises(TypeError, d.pop, 4, 2, 3) d.setdefault('g', []).append(4) eq(d, MultiDict([('a', 5), ('f', 42), ('g', [4])])) def test_multidict_init(): d = MultiDict([('a', 'b')], c=2) eq(repr(d), "MultiDict([('a', 'b'), ('c', 2)])") eq(d, MultiDict([('a', 'b')], c=2)) # TypeError: MultiDict can only be called with one positional argument assert_raises(TypeError, MultiDict, 1, 2, 3) # TypeError: MultiDict.view_list(obj) takes only actual list objects, not None assert_raises(TypeError, MultiDict.view_list, None) WebOb-1.3.1/tests/__init__.py0000644000175000017500000000000211635430213016547 0ustar chrismchrism00000000000000# WebOb-1.3.1/tests/test_request_nose.py0000644000175000017500000001351411677410451020607 0ustar chrismchrism00000000000000from webob.request import Request from nose.tools import eq_ as eq, assert_raises from webob.compat import bytes_ def test_request_no_method(): assert Request({}).method == 'GET' def test_request_read_no_content_length(): req, input = _make_read_tracked_request(b'abc', 'FOO') assert req.content_length is None assert req.body == b'' assert not input.was_read def test_request_read_no_content_length_POST(): req, input = _make_read_tracked_request(b'abc', 'POST') assert req.content_length is None assert req.body == b'abc' assert input.was_read def test_request_read_no_flag_but_content_length_is_present(): req, input = _make_read_tracked_request(b'abc') req.content_length = 3 assert req.body == b'abc' assert input.was_read def test_request_read_no_content_length_but_flagged_readable(): req, input = _make_read_tracked_request(b'abc') req.is_body_readable = True assert req.body == b'abc' assert input.was_read def test_request_read_after_setting_body_file(): req = _make_read_tracked_request()[0] input = req.body_file = ReadTracker(b'abc') assert req.content_length is None assert not req.is_body_seekable assert req.body == b'abc' # reading body made the input seekable and set the clen assert req.content_length == 3 assert req.is_body_seekable assert input.was_read def test_request_readlines(): req = Request.blank('/', POST='a\n'*3) req.is_body_seekable = False eq(req.body_file.readlines(), [b'a\n'] * 3) def test_request_delete_with_body(): req = Request.blank('/', method='DELETE') assert not req.is_body_readable req.body = b'abc' assert req.is_body_readable assert req.body_file.read() == b'abc' def _make_read_tracked_request(data='', method='PUT'): input = ReadTracker(data) env = { 'REQUEST_METHOD': method, 'wsgi.input': input, } return Request(env), input class ReadTracker(object): """ Helper object to determine if the input was read or not """ def __init__(self, data): self.data = data self.was_read = False def read(self, size=-1): if size < 0: size = len(self.data) assert size == len(self.data) self.was_read = True return self.data def test_limited_length_file_repr(): req = Request.blank('/', POST='x') req.body_file_raw = 'dummy' req.is_body_seekable = False eq(repr(req.body_file.raw), "") def test_request_wrong_clen(is_seekable=False): tlen = 1<<20 req = Request.blank('/', POST='x'*tlen) eq(req.content_length, tlen) req.body_file = _Helper_test_request_wrong_clen(req.body_file) eq(req.content_length, None) req.content_length = tlen + 100 req.is_body_seekable = is_seekable eq(req.content_length, tlen+100) # this raises AssertionError if the body reading # trusts content_length too much assert_raises(IOError, req.copy_body) def test_request_wrong_clen_seekable(): test_request_wrong_clen(is_seekable=True) def test_webob_version(): import webob assert isinstance(webob.__version__, str) class _Helper_test_request_wrong_clen(object): def __init__(self, f): self.f = f self.file_ended = False def read(self, *args): r = self.f.read(*args) if not r: if self.file_ended: raise AssertionError("Reading should stop after first empty string") self.file_ended = True return r def test_disconnect_detection_cgi(): data = 'abc'*(1<<20) req = Request.blank('/', POST={'file':('test-file', data)}) req.is_body_seekable = False req.POST # should not raise exceptions def test_disconnect_detection_hinted_readline(): data = 'abc'*(1<<20) req = Request.blank('/', POST=data) req.is_body_seekable = False line = req.body_file.readline(1<<16) assert line assert bytes_(data).startswith(line) def test_charset_in_content_type(): # should raise no exception req = Request({ 'REQUEST_METHOD': 'POST', 'QUERY_STRING':'a=b', 'CONTENT_TYPE':'text/html;charset=ascii' }) eq(req.charset, 'ascii') eq(dict(req.GET), {'a': 'b'}) eq(dict(req.POST), {}) req.charset = 'ascii' # no exception assert_raises(DeprecationWarning, setattr, req, 'charset', 'utf-8') # again no exception req = Request({ 'REQUEST_METHOD': 'POST', 'QUERY_STRING':'a=b', 'CONTENT_TYPE':'multipart/form-data;charset=ascii' }) eq(req.charset, 'ascii') eq(dict(req.GET), {'a': 'b'}) assert_raises(DeprecationWarning, getattr, req, 'POST') def test_json_body_invalid_json(): request = Request.blank('/', POST=b'{') assert_raises(ValueError, getattr, request, 'json_body') def test_json_body_valid_json(): request = Request.blank('/', POST=b'{"a":1}') eq(request.json_body, {'a':1}) def test_json_body_alternate_charset(): import json body = (b'\xff\xfe{\x00"\x00a\x00"\x00:\x00 \x00"\x00/\x00\\\x00u\x006\x00d\x004\x001\x00' b'\\\x00u\x008\x008\x004\x00c\x00\\\x00u\x008\x00d\x008\x00b\x00\\\x00u\x005\x002\x00' b'b\x00f\x00"\x00}\x00' ) request = Request.blank('/', POST=body) request.content_type = 'application/json; charset=utf-16' s = request.json_body['a'] eq(s.encode('utf8'), b'/\xe6\xb5\x81\xe8\xa1\x8c\xe8\xb6\x8b\xe5\x8a\xbf') def test_json_body_GET_request(): request = Request.blank('/') assert_raises(ValueError, getattr, request, 'json_body') def test_non_ascii_body_params(): body = b'test=%D1%82%D0%B5%D1%81%D1%82' req = Request.blank('/', POST=body) # acessing params parses request body req.params # accessing body again makes the POST dict serialize again # make sure it can handle the non-ascii characters in the query eq(req.body, body) WebOb-1.3.1/tests/test_descriptors.py0000664000175000017500000006230012123261311020415 0ustar chrismchrism00000000000000# -*- coding: utf-8 -*- import unittest from webob.compat import ( PY3, text_, native_, ) from datetime import tzinfo from datetime import timedelta from nose.tools import eq_ from nose.tools import ok_ from nose.tools import assert_raises from webob.request import Request class GMT(tzinfo): """UTC""" ZERO = timedelta(0) def utcoffset(self, dt): return self.ZERO def tzname(self, dt): return "UTC" def dst(self, dt): return self.ZERO class MockDescriptor: _val = 'avalue' def __get__(self, obj, type=None): return self._val def __set__(self, obj, val): self._val = val def __delete__(self, obj): self._val = None def test_environ_getter_docstring(): from webob.descriptors import environ_getter desc = environ_getter('akey') eq_(desc.__doc__, "Gets and sets the ``akey`` key in the environment.") def test_environ_getter_nodefault_keyerror(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey') assert_raises(KeyError, desc.fget, req) def test_environ_getter_nodefault_fget(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey') desc.fset(req, 'bar') eq_(req.environ['akey'], 'bar') def test_environ_getter_nodefault_fdel(): from webob.descriptors import environ_getter desc = environ_getter('akey') eq_(desc.fdel, None) def test_environ_getter_default_fget(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey', default='the_default') eq_(desc.fget(req), 'the_default') def test_environ_getter_default_fset(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey', default='the_default') desc.fset(req, 'bar') eq_(req.environ['akey'], 'bar') def test_environ_getter_default_fset_none(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey', default='the_default') desc.fset(req, 'baz') desc.fset(req, None) ok_('akey' not in req.environ) def test_environ_getter_default_fdel(): from webob.descriptors import environ_getter req = Request.blank('/') desc = environ_getter('akey', default='the_default') desc.fset(req, 'baz') assert 'akey' in req.environ desc.fdel(req) ok_('akey' not in req.environ) def test_environ_getter_rfc_section(): from webob.descriptors import environ_getter desc = environ_getter('HTTP_X_AKEY', rfc_section='14.3') eq_(desc.__doc__, "Gets and sets the ``X-Akey`` header " "(`HTTP spec section 14.3 " "`_)." ) def test_upath_property_fget(): from webob.descriptors import upath_property req = Request.blank('/') desc = upath_property('akey') eq_(desc.fget(req), '') def test_upath_property_fset(): from webob.descriptors import upath_property req = Request.blank('/') desc = upath_property('akey') desc.fset(req, 'avalue') eq_(desc.fget(req), 'avalue') def test_header_getter_doc(): from webob.descriptors import header_getter desc = header_getter('X-Header', '14.3') assert('http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.3' in desc.__doc__) assert '``X-Header`` header' in desc.__doc__ def test_header_getter_fget(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') eq_(desc.fget(resp), None) def test_header_getter_fset(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue') eq_(desc.fget(resp), 'avalue') def test_header_getter_fset_none(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue') desc.fset(resp, None) eq_(desc.fget(resp), None) def test_header_getter_fset_text(): from webob.compat import text_ from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, text_('avalue')) eq_(desc.fget(resp), 'avalue') def test_header_getter_fdel(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue2') desc.fdel(resp) eq_(desc.fget(resp), None) def test_header_getter_unicode_fget_none(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') eq_(desc.fget(resp), None) def test_header_getter_unicode_fget(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue') eq_(desc.fget(resp), 'avalue') def test_header_getter_unicode_fset_none(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, None) eq_(desc.fget(resp), None) def test_header_getter_unicode_fset(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue2') eq_(desc.fget(resp), 'avalue2') def test_header_getter_unicode_fdel(): from webob.descriptors import header_getter from webob import Response resp = Response('aresp') desc = header_getter('AHEADER', '14.3') desc.fset(resp, 'avalue3') desc.fdel(resp) eq_(desc.fget(resp), None) def test_converter_not_prop(): from webob.descriptors import converter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int assert_raises(AssertionError,converter, ('CONTENT_LENGTH', None, '14.13'), parse_int_safe, serialize_int, 'int') def test_converter_with_name_docstring(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int desc = converter( environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int, 'int') assert 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.13' in desc.__doc__ assert '``Content-Length`` header' in desc.__doc__ def test_converter_with_name_fget(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int, 'int') eq_(desc.fget(req), 666) def test_converter_with_name_fset(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int, 'int') desc.fset(req, '999') eq_(desc.fget(req), 999) def test_converter_without_name_fget(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int) eq_(desc.fget(req), 666) def test_converter_without_name_fset(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int) desc.fset(req, '999') eq_(desc.fget(req), 999) def test_converter_none_for_wrong_type(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( ## XXX: Should this fail if the type is wrong? environ_getter('CONTENT_LENGTH', 'sixsixsix', '14.13'), parse_int_safe, serialize_int, 'int') eq_(desc.fget(req), None) def test_converter_delete(): from webob.descriptors import converter from webob.descriptors import environ_getter from webob.descriptors import parse_int_safe from webob.descriptors import serialize_int req = Request.blank('/') desc = converter( ## XXX: Should this fail if the type is wrong? environ_getter('CONTENT_LENGTH', '666', '14.13'), parse_int_safe, serialize_int, 'int') assert_raises(KeyError, desc.fdel, req) def test_list_header(): from webob.descriptors import list_header desc = list_header('CONTENT_LENGTH', '14.13') eq_(type(desc), property) def test_parse_list_single(): from webob.descriptors import parse_list result = parse_list('avalue') eq_(result, ('avalue',)) def test_parse_list_multiple(): from webob.descriptors import parse_list result = parse_list('avalue,avalue2') eq_(result, ('avalue', 'avalue2')) def test_parse_list_none(): from webob.descriptors import parse_list result = parse_list(None) eq_(result, None) def test_parse_list_unicode_single(): from webob.descriptors import parse_list result = parse_list('avalue') eq_(result, ('avalue',)) def test_parse_list_unicode_multiple(): from webob.descriptors import parse_list result = parse_list('avalue,avalue2') eq_(result, ('avalue', 'avalue2')) def test_serialize_list(): from webob.descriptors import serialize_list result = serialize_list(('avalue', 'avalue2')) eq_(result, 'avalue, avalue2') def test_serialize_list_string(): from webob.descriptors import serialize_list result = serialize_list('avalue') eq_(result, 'avalue') def test_serialize_list_unicode(): from webob.descriptors import serialize_list result = serialize_list('avalue') eq_(result, 'avalue') def test_converter_date(): import datetime from webob.descriptors import converter_date from webob.descriptors import environ_getter req = Request.blank('/') UTC = GMT() desc = converter_date(environ_getter( "HTTP_DATE", "Tue, 15 Nov 1994 08:12:31 GMT", "14.8")) eq_(desc.fget(req), datetime.datetime(1994, 11, 15, 8, 12, 31, tzinfo=UTC)) def test_converter_date_docstring(): from webob.descriptors import converter_date from webob.descriptors import environ_getter desc = converter_date(environ_getter( "HTTP_DATE", "Tue, 15 Nov 1994 08:12:31 GMT", "14.8")) assert 'http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.8' in desc.__doc__ assert '``Date`` header' in desc.__doc__ def test_date_header_fget_none(): from webob import Response from webob.descriptors import date_header resp = Response('aresponse') desc = date_header('HTTP_DATE', "14.8") eq_(desc.fget(resp), None) def test_date_header_fset_fget(): import datetime from webob import Response from webob.descriptors import date_header resp = Response('aresponse') UTC = GMT() desc = date_header('HTTP_DATE', "14.8") desc.fset(resp, "Tue, 15 Nov 1994 08:12:31 GMT") eq_(desc.fget(resp), datetime.datetime(1994, 11, 15, 8, 12, 31, tzinfo=UTC)) def test_date_header_fdel(): from webob import Response from webob.descriptors import date_header resp = Response('aresponse') desc = date_header('HTTP_DATE', "14.8") desc.fset(resp, "Tue, 15 Nov 1994 08:12:31 GMT") desc.fdel(resp) eq_(desc.fget(resp), None) def test_deprecated_property(): from webob.descriptors import deprecated_property class Foo(object): pass Foo.attr = deprecated_property('attr', 'attr', 'whatever', '1.2') foo = Foo() assert_raises(DeprecationWarning, getattr, foo, 'attr') assert_raises(DeprecationWarning, setattr, foo, 'attr', {}) assert_raises(DeprecationWarning, delattr, foo, 'attr') def test_parse_etag_response(): from webob.descriptors import parse_etag_response etresp = parse_etag_response("etag") eq_(etresp, "etag") def test_parse_etag_response_quoted(): from webob.descriptors import parse_etag_response etresp = parse_etag_response('"etag"') eq_(etresp, "etag") def test_parse_etag_response_is_none(): from webob.descriptors import parse_etag_response etresp = parse_etag_response(None) eq_(etresp, None) def test_serialize_etag_response(): from webob.descriptors import serialize_etag_response etresp = serialize_etag_response("etag") eq_(etresp, '"etag"') def test_serialize_if_range_string(): from webob.descriptors import serialize_if_range val = serialize_if_range("avalue") eq_(val, "avalue") def test_serialize_if_range_unicode(): from webob.descriptors import serialize_if_range val = serialize_if_range("avalue") eq_(val, "avalue") def test_serialize_if_range_datetime(): import datetime from webob.descriptors import serialize_if_range UTC = GMT() val = serialize_if_range(datetime.datetime(1994, 11, 15, 8, 12, 31, tzinfo=UTC)) eq_(val, "Tue, 15 Nov 1994 08:12:31 GMT") def test_serialize_if_range_other(): from webob.descriptors import serialize_if_range val = serialize_if_range(123456) eq_(val, '123456') def test_parse_range_none(): from webob.descriptors import parse_range eq_(parse_range(None), None) def test_parse_range_type(): from webob.byterange import Range from webob.descriptors import parse_range val = parse_range("bytes=1-500") eq_(type(val), type(Range.parse("bytes=1-500"))) def test_parse_range_values(): from webob.byterange import Range range = Range.parse("bytes=1-500") eq_(range.start, 1) eq_(range.end, 501) def test_serialize_range_none(): from webob.descriptors import serialize_range val = serialize_range(None) eq_(val, None) def test_serialize_range(): from webob.descriptors import serialize_range val = serialize_range((1,500)) eq_(val, 'bytes=1-499') def test_parse_int_none(): from webob.descriptors import parse_int val = parse_int(None) eq_(val, None) def test_parse_int_emptystr(): from webob.descriptors import parse_int val = parse_int('') eq_(val, None) def test_parse_int(): from webob.descriptors import parse_int val = parse_int('123') eq_(val, 123) def test_parse_int_invalid(): from webob.descriptors import parse_int assert_raises(ValueError, parse_int, 'abc') def test_parse_int_safe_none(): from webob.descriptors import parse_int_safe eq_(parse_int_safe(None), None) def test_parse_int_safe_emptystr(): from webob.descriptors import parse_int_safe eq_(parse_int_safe(''), None) def test_parse_int_safe(): from webob.descriptors import parse_int_safe eq_(parse_int_safe('123'), 123) def test_parse_int_safe_invalid(): from webob.descriptors import parse_int_safe eq_(parse_int_safe('abc'), None) def test_serialize_int(): from webob.descriptors import serialize_int assert serialize_int is str def test_parse_content_range_none(): from webob.descriptors import parse_content_range eq_(parse_content_range(None), None) def test_parse_content_range_emptystr(): from webob.descriptors import parse_content_range eq_(parse_content_range(' '), None) def test_parse_content_range_length(): from webob.byterange import ContentRange from webob.descriptors import parse_content_range val = parse_content_range("bytes 0-499/1234") eq_(val.length, ContentRange.parse("bytes 0-499/1234").length) def test_parse_content_range_start(): from webob.byterange import ContentRange from webob.descriptors import parse_content_range val = parse_content_range("bytes 0-499/1234") eq_(val.start, ContentRange.parse("bytes 0-499/1234").start) def test_parse_content_range_stop(): from webob.byterange import ContentRange from webob.descriptors import parse_content_range val = parse_content_range("bytes 0-499/1234") eq_(val.stop, ContentRange.parse("bytes 0-499/1234").stop) def test_serialize_content_range_none(): from webob.descriptors import serialize_content_range eq_(serialize_content_range(None), 'None') ### XXX: Seems wrong def test_serialize_content_range_emptystr(): from webob.descriptors import serialize_content_range eq_(serialize_content_range(''), None) def test_serialize_content_range_invalid(): from webob.descriptors import serialize_content_range assert_raises(ValueError, serialize_content_range, (1,)) def test_serialize_content_range_asterisk(): from webob.descriptors import serialize_content_range eq_(serialize_content_range((0, 500)), 'bytes 0-499/*') def test_serialize_content_range_defined(): from webob.descriptors import serialize_content_range eq_(serialize_content_range((0, 500, 1234)), 'bytes 0-499/1234') def test_parse_auth_params_leading_capital_letter(): from webob.descriptors import parse_auth_params val = parse_auth_params('Basic Realm=WebOb') eq_(val, {'ealm': 'WebOb'}) def test_parse_auth_params_trailing_capital_letter(): from webob.descriptors import parse_auth_params val = parse_auth_params('Basic realM=WebOb') eq_(val, {}) def test_parse_auth_params_doublequotes(): from webob.descriptors import parse_auth_params val = parse_auth_params('Basic realm="Web Object"') eq_(val, {'realm': 'Web Object'}) def test_parse_auth_params_multiple_values(): from webob.descriptors import parse_auth_params val = parse_auth_params("foo='blah &&234', qop=foo, nonce='qwerty1234'") eq_(val, {'nonce': "'qwerty1234'", 'foo': "'blah &&234'", 'qop': 'foo'}) def test_parse_auth_params_truncate_on_comma(): from webob.descriptors import parse_auth_params val = parse_auth_params("Basic realm=WebOb,this_will_truncate") eq_(val, {'realm': 'WebOb'}) def test_parse_auth_params_emptystr(): from webob.descriptors import parse_auth_params eq_(parse_auth_params(''), {}) def test_authorization2(): from webob.descriptors import parse_auth_params for s, d in [ ('x=y', {'x': 'y'}), ('x="y"', {'x': 'y'}), ('x=y,z=z', {'x': 'y', 'z': 'z'}), ('x=y, z=z', {'x': 'y', 'z': 'z'}), ('x="y",z=z', {'x': 'y', 'z': 'z'}), ('x="y", z=z', {'x': 'y', 'z': 'z'}), ('x="y,x", z=z', {'x': 'y,x', 'z': 'z'}), ]: eq_(parse_auth_params(s), d) def test_parse_auth_none(): from webob.descriptors import parse_auth eq_(parse_auth(None), None) def test_parse_auth_emptystr(): from webob.descriptors import parse_auth assert_raises(ValueError, parse_auth, '') def test_parse_auth_basic(): from webob.descriptors import parse_auth eq_(parse_auth("Basic realm=WebOb"), ('Basic', 'realm=WebOb')) def test_parse_auth_basic_quoted(): from webob.descriptors import parse_auth eq_(parse_auth('Basic realm="Web Ob"'), ('Basic', {'realm': 'Web Ob'})) def test_parse_auth_basic_quoted_multiple_unknown(): from webob.descriptors import parse_auth eq_(parse_auth("foo='blah &&234', qop=foo, nonce='qwerty1234'"), ("foo='blah", "&&234', qop=foo, nonce='qwerty1234'")) def test_parse_auth_basic_quoted_known_multiple(): from webob.descriptors import parse_auth eq_(parse_auth("Basic realm='blah &&234', qop=foo, nonce='qwerty1234'"), ('Basic', "realm='blah &&234', qop=foo, nonce='qwerty1234'")) def test_serialize_auth_none(): from webob.descriptors import serialize_auth eq_(serialize_auth(None), None) def test_serialize_auth_emptystr(): from webob.descriptors import serialize_auth eq_(serialize_auth(''), '') def test_serialize_auth_basic_quoted(): from webob.descriptors import serialize_auth val = serialize_auth(('Basic', 'realm="WebOb"')) eq_(val, 'Basic realm="WebOb"') def test_serialize_auth_digest_multiple(): from webob.descriptors import serialize_auth val = serialize_auth(('Digest', 'realm="WebOb", nonce=abcde12345, qop=foo')) flags = val[len('Digest'):] result = sorted([ x.strip() for x in flags.split(',') ]) eq_(result, ['nonce=abcde12345', 'qop=foo', 'realm="WebOb"']) def test_serialize_auth_digest_tuple(): from webob.descriptors import serialize_auth val = serialize_auth(('Digest', {'realm':'"WebOb"', 'nonce':'abcde12345', 'qop':'foo'})) flags = val[len('Digest'):] result = sorted([ x.strip() for x in flags.split(',') ]) eq_(result, ['nonce="abcde12345"', 'qop="foo"', 'realm=""WebOb""']) _nodefault = object() class _TestEnvironDecoder(object): def _callFUT(self, key, default=_nodefault, rfc_section=None, encattr=None): from webob.descriptors import environ_decoder if default is _nodefault: return environ_decoder(key, rfc_section=rfc_section, encattr=encattr) else: return environ_decoder(key, default=default, rfc_section=rfc_section, encattr=encattr) def test_docstring(self): desc = self._callFUT('akey') self.assertEqual(desc.__doc__, "Gets and sets the ``akey`` key in the environment.") def test_nodefault_keyerror(self): req = self._makeRequest() desc = self._callFUT('akey') self.assertRaises(KeyError, desc.fget, req) def test_nodefault_fget(self): req = self._makeRequest() desc = self._callFUT('akey') desc.fset(req, 'bar') self.assertEqual(req.environ['akey'], 'bar') def test_nodefault_fdel(self): desc = self._callFUT('akey') self.assertEqual(desc.fdel, None) def test_default_fget(self): req = self._makeRequest() desc = self._callFUT('akey', default='the_default') self.assertEqual(desc.fget(req), 'the_default') def test_default_fset(self): req = self._makeRequest() desc = self._callFUT('akey', default='the_default') desc.fset(req, 'bar') self.assertEqual(req.environ['akey'], 'bar') def test_default_fset_none(self): req = self._makeRequest() desc = self._callFUT('akey', default='the_default') desc.fset(req, 'baz') desc.fset(req, None) self.assertTrue('akey' not in req.environ) def test_default_fdel(self): req = self._makeRequest() desc = self._callFUT('akey', default='the_default') desc.fset(req, 'baz') self.assertTrue('akey' in req.environ) desc.fdel(req) self.assertTrue('akey' not in req.environ) def test_rfc_section(self): desc = self._callFUT('HTTP_X_AKEY', rfc_section='14.3') self.assertEqual( desc.__doc__, "Gets and sets the ``X-Akey`` header " "(`HTTP spec section 14.3 " "`_)." ) def test_fset_nonascii(self): desc = self._callFUT('HTTP_X_AKEY', encattr='url_encoding') req = self._makeRequest() desc.fset(req, text_(b'\xc3\xab', 'utf-8')) if PY3: self.assertEqual(req.environ['HTTP_X_AKEY'], b'\xc3\xab'.decode('latin-1')) else: self.assertEqual(req.environ['HTTP_X_AKEY'], b'\xc3\xab') class TestEnvironDecoder(unittest.TestCase, _TestEnvironDecoder): def _makeRequest(self): from webob.request import BaseRequest req = BaseRequest.blank('/') return req def test_fget_nonascii(self): desc = self._callFUT('HTTP_X_AKEY', encattr='url_encoding') req = self._makeRequest() if PY3: req.environ['HTTP_X_AKEY'] = b'\xc3\xab'.decode('latin-1') else: req.environ['HTTP_X_AKEY'] = b'\xc3\xab' result = desc.fget(req) self.assertEqual(result, text_(b'\xc3\xab', 'utf-8')) class TestEnvironDecoderLegacy(unittest.TestCase, _TestEnvironDecoder): def _makeRequest(self): from webob.request import LegacyRequest req = LegacyRequest.blank('/') return req def test_fget_nonascii(self): desc = self._callFUT('HTTP_X_AKEY', encattr='url_encoding') req = self._makeRequest() if PY3: req.environ['HTTP_X_AKEY'] = b'\xc3\xab'.decode('latin-1') else: req.environ['HTTP_X_AKEY'] = b'\xc3\xab' result = desc.fget(req) self.assertEqual(result, native_(b'\xc3\xab', 'latin-1')) def test_default_fget_nonascii(self): req = self._makeRequest() desc = self._callFUT('akey', default=b'the_default') self.assertEqual(desc.fget(req).__class__, bytes) WebOb-1.3.1/tests/test_headers.py0000664000175000017500000000600412123261311017466 0ustar chrismchrism00000000000000# -*- coding: utf-8 -*- from webob import headers from nose.tools import ok_, assert_raises, eq_ class TestError(Exception): pass def test_ResponseHeaders_delitem_notpresent(): """Deleting a missing key from ResponseHeaders should raise a KeyError""" d = headers.ResponseHeaders() assert_raises(KeyError, d.__delitem__, 'b') def test_ResponseHeaders_delitem_present(): """ Deleting a present key should not raise an error at all """ d = headers.ResponseHeaders(a=1) del d['a'] ok_('a' not in d) def test_ResponseHeaders_setdefault(): """Testing set_default for ResponseHeaders""" d = headers.ResponseHeaders(a=1) res = d.setdefault('b', 1) assert res == d['b'] == 1 res = d.setdefault('b', 10) assert res == d['b'] == 1 res = d.setdefault('B', 10) assert res == d['b'] == d['B'] == 1 def test_ResponseHeader_pop(): """Testing if pop return TypeError when more than len(*args)>1 plus other assorted tests""" d = headers.ResponseHeaders(a=1, b=2, c=3, d=4) assert_raises(TypeError, d.pop, 'a', 'z', 'y') eq_(d.pop('a'), 1) ok_('a' not in d) eq_(d.pop('B'), 2) ok_('b' not in d) eq_(d.pop('c', 'u'), 3) ok_('c' not in d) eq_(d.pop('e', 'u'), 'u') ok_('e' not in d) assert_raises(KeyError, d.pop, 'z') def test_ResponseHeaders_getitem_miss(): d = headers.ResponseHeaders() assert_raises(KeyError, d.__getitem__, 'a') def test_ResponseHeaders_getall(): d = headers.ResponseHeaders() d.add('a', 1) d.add('a', 2) result = d.getall('a') eq_(result, [1,2]) def test_ResponseHeaders_mixed(): d = headers.ResponseHeaders() d.add('a', 1) d.add('a', 2) d['b'] = 1 result = d.mixed() eq_(result, {'a':[1,2], 'b':1}) def test_ResponseHeaders_setitem_scalar_replaces_seq(): d = headers.ResponseHeaders() d.add('a', 2) d['a'] = 1 result = d.getall('a') eq_(result, [1]) def test_ResponseHeaders_contains(): d = headers.ResponseHeaders() d['a'] = 1 ok_('a' in d) ok_(not 'b' in d) def test_EnvironHeaders_delitem(): d = headers.EnvironHeaders({'CONTENT_LENGTH': '10'}) del d['CONTENT-LENGTH'] assert not d assert_raises(KeyError, d.__delitem__, 'CONTENT-LENGTH') def test_EnvironHeaders_getitem(): d = headers.EnvironHeaders({'CONTENT_LENGTH': '10'}) eq_(d['CONTENT-LENGTH'], '10') def test_EnvironHeaders_setitem(): d = headers.EnvironHeaders({}) d['abc'] = '10' eq_(d['abc'], '10') def test_EnvironHeaders_contains(): d = headers.EnvironHeaders({}) d['a'] = '10' ok_('a' in d) ok_(not 'b' in d) def test__trans_key_not_basestring(): result = headers._trans_key(None) eq_(result, None) def test__trans_key_not_a_header(): result = headers._trans_key('') eq_(result, None) def test__trans_key_key2header(): result = headers._trans_key('CONTENT_TYPE') eq_(result, 'Content-Type') def test__trans_key_httpheader(): result = headers._trans_key('HTTP_FOO_BAR') eq_(result, 'Foo-Bar') WebOb-1.3.1/tests/test_static.py0000664000175000017500000002132312023145137017351 0ustar chrismchrism00000000000000from io import BytesIO from os.path import getmtime import tempfile from time import gmtime import os import shutil import unittest from webob import static from webob.compat import bytes_ from webob.request import Request, environ_from_url from webob.response import Response def get_response(app, path='/', **req_kw): """Convenient function to query an application""" req = Request(environ_from_url(path), **req_kw) return req.get_response(app) def create_file(content, *paths): """Convenient function to create a new file with some content""" path = os.path.join(*paths) with open(path, 'wb') as fp: fp.write(bytes_(content)) return path class TestFileApp(unittest.TestCase): def setUp(self): fp = tempfile.NamedTemporaryFile(suffix=".py", delete=False) self.tempfile = fp.name fp.write(b"import this\n") fp.close() def tearDown(self): os.unlink(self.tempfile) def test_fileapp(self): app = static.FileApp(self.tempfile) resp1 = get_response(app) self.assertEqual(resp1.content_type, 'text/x-python') self.assertEqual(resp1.charset, 'UTF-8') self.assertEqual(resp1.last_modified.timetuple(), gmtime(getmtime(self.tempfile))) resp2 = get_response(app) self.assertEqual(resp2.content_type, 'text/x-python') self.assertEqual(resp2.last_modified.timetuple(), gmtime(getmtime(self.tempfile))) resp3 = get_response(app, range=(7, 11)) self.assertEqual(resp3.status_code, 206) self.assertEqual(tuple(resp3.content_range)[:2], (7, 11)) self.assertEqual(resp3.last_modified.timetuple(), gmtime(getmtime(self.tempfile))) self.assertEqual(resp3.body, bytes_('this')) def test_unexisting_file(self): app = static.FileApp('/tmp/this/doesnt/exist') self.assertEqual(404, get_response(app).status_code) def test_allowed_methods(self): app = static.FileApp(self.tempfile) # Alias resp = lambda method: get_response(app, method=method) self.assertEqual(200, resp(method='GET').status_code) self.assertEqual(200, resp(method='HEAD').status_code) self.assertEqual(405, resp(method='POST').status_code) # Actually any other method is not allowed self.assertEqual(405, resp(method='xxx').status_code) def test_exception_while_opening_file(self): # Mock the built-in ``open()`` function to allow finner control about # what we are testing. def open_ioerror(*args, **kwargs): raise IOError() def open_oserror(*args, **kwargs): raise OSError() app = static.FileApp(self.tempfile) app._open = open_ioerror self.assertEqual(403, get_response(app).status_code) app._open = open_oserror self.assertEqual(403, get_response(app).status_code) def test_use_wsgi_filewrapper(self): class TestWrapper(object): def __init__(self, file, block_size): self.file = file self.block_size = block_size environ = environ_from_url('/') environ['wsgi.file_wrapper'] = TestWrapper app = static.FileApp(self.tempfile) app_iter = Request(environ).get_response(app).app_iter self.assertTrue(isinstance(app_iter, TestWrapper)) self.assertEqual(bytes_('import this\n'), app_iter.file.read()) self.assertEqual(static.BLOCK_SIZE, app_iter.block_size) class TestFileIter(unittest.TestCase): def test_empty_file(self): fp = BytesIO() fi = static.FileIter(fp) self.assertRaises(StopIteration, next, iter(fi)) def test_seek(self): fp = BytesIO(bytes_("0123456789")) i = static.FileIter(fp).app_iter_range(seek=4) self.assertEqual(bytes_("456789"), next(i)) self.assertRaises(StopIteration, next, i) def test_limit(self): fp = BytesIO(bytes_("0123456789")) i = static.FileIter(fp).app_iter_range(limit=4) self.assertEqual(bytes_("0123"), next(i)) self.assertRaises(StopIteration, next, i) def test_limit_and_seek(self): fp = BytesIO(bytes_("0123456789")) i = static.FileIter(fp).app_iter_range(limit=4, seek=1) self.assertEqual(bytes_("123"), next(i)) self.assertRaises(StopIteration, next, i) def test_multiple_reads(self): fp = BytesIO(bytes_("012")) i = static.FileIter(fp).app_iter_range(block_size=1) self.assertEqual(bytes_("0"), next(i)) self.assertEqual(bytes_("1"), next(i)) self.assertEqual(bytes_("2"), next(i)) self.assertRaises(StopIteration, next, i) def test_seek_bigger_than_limit(self): fp = BytesIO(bytes_("0123456789")) i = static.FileIter(fp).app_iter_range(limit=1, seek=2) # XXX: this should not return anything actually, since we are starting # to read after the place we wanted to stop. self.assertEqual(bytes_("23456789"), next(i)) self.assertRaises(StopIteration, next, i) def test_limit_is_zero(self): fp = BytesIO(bytes_("0123456789")) i = static.FileIter(fp).app_iter_range(limit=0) self.assertRaises(StopIteration, next, i) class TestDirectoryApp(unittest.TestCase): def setUp(self): self.test_dir = tempfile.mkdtemp() def tearDown(self): shutil.rmtree(self.test_dir) def test_empty_directory(self): app = static.DirectoryApp(self.test_dir) self.assertEqual(404, get_response(app).status_code) self.assertEqual(404, get_response(app, '/foo').status_code) def test_serve_file(self): app = static.DirectoryApp(self.test_dir) create_file('abcde', self.test_dir, 'bar') self.assertEqual(404, get_response(app).status_code) self.assertEqual(404, get_response(app, '/foo').status_code) resp = get_response(app, '/bar') self.assertEqual(200, resp.status_code) self.assertEqual(bytes_('abcde'), resp.body) def test_dont_serve_file_in_parent_directory(self): # We'll have: # /TEST_DIR/ # /TEST_DIR/bar # /TEST_DIR/foo/ <- serve this directory create_file('abcde', self.test_dir, 'bar') serve_path = os.path.join(self.test_dir, 'foo') os.mkdir(serve_path) app = static.DirectoryApp(serve_path) # The file exists, but is outside the served dir. self.assertEqual(403, get_response(app, '/../bar').status_code) def test_file_app_arguments(self): app = static.DirectoryApp(self.test_dir, content_type='xxx/yyy') create_file('abcde', self.test_dir, 'bar') resp = get_response(app, '/bar') self.assertEqual(200, resp.status_code) self.assertEqual('xxx/yyy', resp.content_type) def test_file_app_factory(self): def make_fileapp(*args, **kwargs): make_fileapp.called = True return Response() make_fileapp.called = False app = static.DirectoryApp(self.test_dir) app.make_fileapp = make_fileapp create_file('abcde', self.test_dir, 'bar') get_response(app, '/bar') self.assertTrue(make_fileapp.called) def test_must_serve_directory(self): serve_path = create_file('abcde', self.test_dir, 'bar') self.assertRaises(IOError, static.DirectoryApp, serve_path) def test_index_page(self): os.mkdir(os.path.join(self.test_dir, 'index-test')) create_file(bytes_('index'), self.test_dir, 'index-test', 'index.html') app = static.DirectoryApp(self.test_dir) resp = get_response(app, '/index-test') self.assertEqual(resp.status_code, 301) self.assertTrue(resp.location.endswith('/index-test/')) resp = get_response(app, '/index-test?test') self.assertTrue(resp.location.endswith('/index-test/?test')) resp = get_response(app, '/index-test/') self.assertEqual(resp.status_code, 200) self.assertEqual(resp.body, bytes_('index')) self.assertEqual(resp.content_type, 'text/html') resp = get_response(app, '/index-test/index.html') self.assertEqual(resp.status_code, 200) self.assertEqual(resp.body, bytes_('index')) redir_app = static.DirectoryApp(self.test_dir, hide_index_with_redirect=True) resp = get_response(redir_app, '/index-test/index.html') self.assertEqual(resp.status_code, 301) self.assertTrue(resp.location.endswith('/index-test/')) resp = get_response(redir_app, '/index-test/index.html?test') self.assertTrue(resp.location.endswith('/index-test/?test')) page_app = static.DirectoryApp(self.test_dir, index_page='something-else.html') self.assertEqual(get_response(page_app, '/index-test/').status_code, 404) WebOb-1.3.1/tests/test_multidict.py0000664000175000017500000003364212023145137020067 0ustar chrismchrism00000000000000# -*- coding: utf-8 -*- import unittest from webob import multidict from webob.compat import text_ class BaseDictTests(object): def setUp(self): self._list = [('a', text_('\xe9')), ('a', 'e'), ('a', 'f'), ('b', '1')] self.data = multidict.MultiDict(self._list) self.d = self._get_instance() def _get_instance(self, **kwargs): if kwargs: data = multidict.MultiDict(kwargs) else: data = self.data.copy() return self.klass(data) def test_len(self): self.assertEqual(len(self.d), 4) def test_getone(self): self.assertEqual(self.d.getone('b'), '1') def test_getone_missing(self): self.assertRaises(KeyError, self.d.getone, 'z') def test_getone_multiple_raises(self): self.assertRaises(KeyError, self.d.getone, 'a') def test_getall(self): self.assertEqual(list(self.d.getall('b')), ['1']) def test_dict_of_lists(self): self.assertEqual( self.d.dict_of_lists(), {'a': [text_('\xe9'), 'e', 'f'], 'b': ['1']}) def test_dict_api(self): self.assertTrue('a' in self.d.mixed()) self.assertTrue('a' in self.d.keys()) self.assertTrue('a' in self.d.iterkeys()) self.assertTrue(('b', '1') in self.d.items()) self.assertTrue(('b', '1') in self.d.iteritems()) self.assertTrue('1' in self.d.values()) self.assertTrue('1' in self.d.itervalues()) self.assertEqual(len(self.d), 4) def test_set_del_item(self): d = self._get_instance() self.assertTrue('a' in d) del d['a'] self.assertTrue(not 'a' in d) def test_pop(self): d = self._get_instance() d['a'] = '1' self.assertEqual(d.pop('a'), '1') self.assertEqual(d.pop('x', '1'), '1') def test_pop_wrong_args(self): d = self._get_instance() self.assertRaises(TypeError, d.pop, 'a', '1', '1') def test_pop_missing(self): d = self._get_instance() self.assertRaises(KeyError, d.pop, 'z') def test_popitem(self): d = self._get_instance() self.assertEqual(d.popitem(), ('b', '1')) def test_update(self): d = self._get_instance() d.update(e='1') self.assertTrue('e' in d) d.update(dict(x='1')) self.assertTrue('x' in d) d.update([('y', '1')]) self.assertTrue('y' in d) def test_setdefault(self): d = self._get_instance() d.setdefault('a', '1') self.assertNotEqual(d['a'], '1') d.setdefault('e', '1') self.assertTrue('e' in d) def test_add(self): d = multidict.MultiDict({'a': '1'}) d.add('a', '2') self.assertEqual(list(d.getall('a')), ['1', '2']) d = self._get_instance() d.add('b', '3') self.assertEqual(list(d.getall('b')), ['1', '3']) def test_copy(self): assert self.d.copy() is not self.d if hasattr(self.d, 'multi'): self.assertFalse(self.d.copy().multi is self.d.multi) self.assertFalse(self.d.copy() is self.d.multi) def test_clear(self): d = self._get_instance() d.clear() self.assertEqual(len(d), 0) def test_nonzero(self): d = self._get_instance() self.assertTrue(d) d.clear() self.assertFalse(d) def test_repr(self): self.assertTrue(repr(self._get_instance())) def test_too_many_args(self): from webob.multidict import MultiDict self.assertRaises(TypeError, MultiDict, '1', 2) def test_no_args(self): from webob.multidict import MultiDict md = MultiDict() self.assertEqual(md._items, []) def test_kwargs(self): from webob.multidict import MultiDict md = MultiDict(kw1='val1') self.assertEqual(md._items, [('kw1','val1')]) def test_view_list_not_list(self): from webob.multidict import MultiDict d = MultiDict() self.assertRaises(TypeError, d.view_list, 42) def test_view_list(self): from webob.multidict import MultiDict d = MultiDict() self.assertEqual(d.view_list([1,2])._items, [1,2]) def test_from_fieldstorage_with_filename(self): from webob.multidict import MultiDict d = MultiDict() fs = DummyFieldStorage('a', '1', 'file') self.assertEqual(d.from_fieldstorage(fs), MultiDict({'a':fs.list[0]})) def test_from_fieldstorage_without_filename(self): from webob.multidict import MultiDict d = MultiDict() fs = DummyFieldStorage('a', '1') self.assertEqual(d.from_fieldstorage(fs), MultiDict({'a':'1'})) def test_from_fieldstorage_with_charset(self): from cgi import FieldStorage from webob.request import BaseRequest from webob.multidict import MultiDict multipart_type = 'multipart/form-data; boundary=foobar' from io import BytesIO body = ( b'--foobar\r\n' b'Content-Disposition: form-data; name="title"\r\n' b'Content-type: text/plain; charset="ISO-2022-JP"\r\n' b'\r\n' b'\x1b$B$3$s$K$A$O\x1b(B' b'\r\n' b'--foobar--') multipart_body = BytesIO(body) environ = BaseRequest.blank('/').environ environ.update(CONTENT_TYPE=multipart_type) environ.update(REQUEST_METHOD='POST') environ.update(CONTENT_LENGTH=len(body)) fs = FieldStorage(multipart_body, environ=environ) vars = MultiDict.from_fieldstorage(fs) self.assertEqual(vars['title'].encode('utf8'), text_('こんにちは', 'utf8').encode('utf8')) def test_from_fieldstorage_with_base64_encoding(self): from cgi import FieldStorage from webob.request import BaseRequest from webob.multidict import MultiDict multipart_type = 'multipart/form-data; boundary=foobar' from io import BytesIO body = ( b'--foobar\r\n' b'Content-Disposition: form-data; name="title"\r\n' b'Content-type: text/plain; charset="ISO-2022-JP"\r\n' b'Content-Transfer-Encoding: base64\r\n' b'\r\n' b'GyRCJDMkcyRLJEEkTxsoQg==' b'\r\n' b'--foobar--') multipart_body = BytesIO(body) environ = BaseRequest.blank('/').environ environ.update(CONTENT_TYPE=multipart_type) environ.update(REQUEST_METHOD='POST') environ.update(CONTENT_LENGTH=len(body)) fs = FieldStorage(multipart_body, environ=environ) vars = MultiDict.from_fieldstorage(fs) self.assertEqual(vars['title'].encode('utf8'), text_('こんにちは', 'utf8').encode('utf8')) def test_from_fieldstorage_with_quoted_printable_encoding(self): from cgi import FieldStorage from webob.request import BaseRequest from webob.multidict import MultiDict multipart_type = 'multipart/form-data; boundary=foobar' from io import BytesIO body = ( b'--foobar\r\n' b'Content-Disposition: form-data; name="title"\r\n' b'Content-type: text/plain; charset="ISO-2022-JP"\r\n' b'Content-Transfer-Encoding: quoted-printable\r\n' b'\r\n' b'=1B$B$3$s$K$A$O=1B(B' b'\r\n' b'--foobar--') multipart_body = BytesIO(body) environ = BaseRequest.blank('/').environ environ.update(CONTENT_TYPE=multipart_type) environ.update(REQUEST_METHOD='POST') environ.update(CONTENT_LENGTH=len(body)) fs = FieldStorage(multipart_body, environ=environ) vars = MultiDict.from_fieldstorage(fs) self.assertEqual(vars['title'].encode('utf8'), text_('こんにちは', 'utf8').encode('utf8')) class MultiDictTestCase(BaseDictTests, unittest.TestCase): klass = multidict.MultiDict def test_update_behavior_warning(self): import warnings class Foo(dict): def __len__(self): return 0 foo = Foo() foo['a'] = 1 d = self._get_instance() with warnings.catch_warnings(record=True) as w: d.update(foo) self.assertEqual(len(w), 1) def test_repr_with_password(self): d = self._get_instance(password='pwd') self.assertEqual(repr(d), "MultiDict([('password', '******')])") class NestedMultiDictTestCase(BaseDictTests, unittest.TestCase): klass = multidict.NestedMultiDict def test_getitem(self): d = self.klass({'a':1}) self.assertEqual(d['a'], 1) def test_getitem_raises(self): d = self._get_instance() self.assertRaises(KeyError, d.__getitem__, 'z') def test_contains(self): d = self._get_instance() assert 'a' in d assert 'z' not in d def test_add(self): d = self._get_instance() self.assertRaises(KeyError, d.add, 'b', 3) def test_set_del_item(self): d = self._get_instance() self.assertRaises(KeyError, d.__delitem__, 'a') self.assertRaises(KeyError, d.__setitem__, 'a', 1) def test_update(self): d = self._get_instance() self.assertRaises(KeyError, d.update, e=1) self.assertRaises(KeyError, d.update, dict(x=1)) self.assertRaises(KeyError, d.update, [('y', 1)]) def test_setdefault(self): d = self._get_instance() self.assertRaises(KeyError, d.setdefault, 'a', 1) def test_pop(self): d = self._get_instance() self.assertRaises(KeyError, d.pop, 'a') self.assertRaises(KeyError, d.pop, 'a', 1) def test_popitem(self): d = self._get_instance() self.assertRaises(KeyError, d.popitem, 'a') def test_pop_wrong_args(self): d = self._get_instance() self.assertRaises(KeyError, d.pop, 'a', 1, 1) def test_clear(self): d = self._get_instance() self.assertRaises(KeyError, d.clear) def test_nonzero(self): d = self._get_instance() self.assertEqual(d.__nonzero__(), True) d.dicts = [{}] self.assertEqual(d.__nonzero__(), False) assert not d class TestGetDict(BaseDictTests, unittest.TestCase): klass = multidict.GetDict def _get_instance(self, **kwargs): if kwargs: data = multidict.MultiDict(kwargs) else: data = self.data.copy() return self.klass(data, {}) def test_inititems(self): #The first argument passed into the __init__ method class Arg: def items(self): return [('a', text_('\xe9')), ('a', 'e'), ('a', 'f'), ('b', 1)] d = self._get_instance() d._items = None d.__init__(Arg(), lambda:None) self.assertEqual(self.d._items, self._list) def test_nullextend(self): d = self._get_instance() self.assertEqual(d.extend(), None) d.extend(test = 'a') self.assertEqual(d['test'], 'a') def test_listextend(self): class Other: def items(self): return [text_('\xe9'), 'e', 'f', 1] other = Other() d = self._get_instance() d.extend(other) _list = [text_('\xe9'), 'e', r'f', 1] for v in _list: self.assertTrue(v in d._items) def test_dictextend(self): class Other: def __getitem__(self, item): return {'a':1, 'b':2, 'c':3}.get(item) def keys(self): return ['a', 'b', 'c'] other = Other() d = self._get_instance() d.extend(other) _list = [('a', 1), ('b', 2), ('c', 3)] for v in _list: self.assertTrue(v in d._items) def test_otherextend(self): class Other(object): def __iter__(self): return iter([('a', 1)]) other = Other() d = self._get_instance() d.extend(other) _list = [('a', 1)] for v in _list: self.assertTrue(v in d._items) def test_repr_with_password(self): d = self._get_instance(password='pwd') self.assertEqual(repr(d), "GET([('password', '******')])") class NoVarsTestCase(unittest.TestCase): klass = multidict.NoVars def _get_instance(self): return self.klass() def test_getitem(self): d = self._get_instance() self.assertRaises(KeyError, d.__getitem__, 'a') def test_setitem(self): d = self._get_instance() self.assertRaises(KeyError, d.__setitem__, 'a') def test_delitem(self): d = self._get_instance() self.assertRaises(KeyError, d.__delitem__, 'a') def test_get(self): d = self._get_instance() self.assertEqual(d.get('a', default = 'b'), 'b') def test_getall(self): d = self._get_instance() self.assertEqual(d.getall('a'), []) def test_getone(self): d = self._get_instance() self.assertRaises(KeyError, d.getone, 'a') def test_mixed(self): d = self._get_instance() self.assertEqual(d.mixed(), {}) def test_contains(self): d = self._get_instance() assert 'a' not in d def test_copy(self): d = self._get_instance() self.assertEqual(d.copy(), d) def test_len(self): d = self._get_instance() self.assertEqual(len(d), 0) def test_repr(self): d = self._get_instance() self.assertEqual(repr(d), '') def test_keys(self): d = self._get_instance() self.assertEqual(list(d.keys()), []) def test_iterkeys(self): d = self._get_instance() self.assertEqual(list(d.iterkeys()), []) class DummyField(object): def __init__(self, name, value, filename=None): self.name = name self.value = value self.filename = filename self.type_options = {} self.headers = {} class DummyFieldStorage(object): def __init__(self, name, value, filename=None): self.list = [DummyField(name, value, filename)] WebOb-1.3.1/tests/test_acceptparse.py0000664000175000017500000002670512047064524020373 0ustar chrismchrism00000000000000from webob.request import Request from webob.acceptparse import Accept from webob.acceptparse import MIMEAccept from webob.acceptparse import NilAccept from webob.acceptparse import NoAccept from webob.acceptparse import accept_property from webob.acceptparse import AcceptLanguage from webob.acceptparse import AcceptCharset from nose.tools import eq_, assert_raises def test_parse_accept_badq(): assert list(Accept.parse("value1; q=0.1.2")) == [('value1', 1)] def test_init_accept_content_type(): accept = Accept('text/html') assert accept._parsed == [('text/html', 1)] def test_init_accept_accept_charset(): accept = AcceptCharset('iso-8859-5, unicode-1-1;q=0.8') assert accept._parsed == [('iso-8859-5', 1), ('unicode-1-1', 0.80000000000000004), ('iso-8859-1', 1)] def test_init_accept_accept_charset_mixedcase(): """3.4 Character Sets [...] HTTP character sets are identified by case-insensitive tokens.""" accept = AcceptCharset('ISO-8859-5, UNICODE-1-1;q=0.8') assert accept._parsed == [('iso-8859-5', 1), ('unicode-1-1', 0.80000000000000004), ('iso-8859-1', 1)] def test_init_accept_accept_charset_with_iso_8859_1(): accept = Accept('iso-8859-1') assert accept._parsed == [('iso-8859-1', 1)] def test_init_accept_accept_charset_wildcard(): accept = Accept('*') assert accept._parsed == [('*', 1)] def test_init_accept_accept_language(): accept = AcceptLanguage('da, en-gb;q=0.8, en;q=0.7') assert accept._parsed == [('da', 1), ('en-gb', 0.80000000000000004), ('en', 0.69999999999999996)] def test_init_accept_invalid_value(): accept = AcceptLanguage('da, q, en-gb;q=0.8') # The "q" value should not be there. assert accept._parsed == [('da', 1), ('en-gb', 0.80000000000000004)] def test_init_accept_invalid_q_value(): accept = AcceptLanguage('da, en-gb;q=foo') # I can't get to cover line 40-41 (webob.acceptparse) as the regex # will prevent from hitting these lines (aconrad) assert accept._parsed == [('da', 1), ('en-gb', 1)] def test_accept_repr(): accept = Accept('text/html') assert repr(accept) == "" def test_accept_str(): accept = Accept('text/html') assert str(accept) == 'text/html' def test_zero_quality(): assert Accept('bar, *;q=0').best_match(['foo']) is None assert 'foo' not in Accept('*;q=0') def test_accept_str_with_q_not_1(): value = 'text/html;q=0.5' accept = Accept(value) assert str(accept) == value def test_accept_str_with_q_not_1_multiple(): value = 'text/html;q=0.5, foo/bar' accept = Accept(value) assert str(accept) == value def test_accept_add_other_accept(): accept = Accept('text/html') + Accept('foo/bar') assert str(accept) == 'text/html, foo/bar' accept += Accept('bar/baz;q=0.5') assert str(accept) == 'text/html, foo/bar, bar/baz;q=0.5' def test_accept_add_other_list_of_tuples(): accept = Accept('text/html') accept += [('foo/bar', 1)] assert str(accept) == 'text/html, foo/bar' accept += [('bar/baz', 0.5)] assert str(accept) == 'text/html, foo/bar, bar/baz;q=0.5' accept += ['she/bangs', 'the/house'] assert str(accept) == ('text/html, foo/bar, bar/baz;q=0.5, ' 'she/bangs, the/house') def test_accept_add_other_dict(): accept = Accept('text/html') accept += {'foo/bar': 1} assert str(accept) == 'text/html, foo/bar' accept += {'bar/baz': 0.5} assert str(accept) == 'text/html, foo/bar, bar/baz;q=0.5' def test_accept_add_other_empty_str(): accept = Accept('text/html') accept += '' assert str(accept) == 'text/html' def test_accept_with_no_value_add_other_str(): accept = Accept('') accept += 'text/html' assert str(accept) == 'text/html' def test_contains(): accept = Accept('text/html') assert 'text/html' in accept def test_contains_not(): accept = Accept('text/html') assert not 'foo/bar' in accept def test_quality(): accept = Accept('text/html') assert accept.quality('text/html') == 1 accept = Accept('text/html;q=0.5') assert accept.quality('text/html') == 0.5 def test_quality_not_found(): accept = Accept('text/html') assert accept.quality('foo/bar') is None def test_best_match(): accept = Accept('text/html, foo/bar') assert accept.best_match(['text/html', 'foo/bar']) == 'text/html' assert accept.best_match(['foo/bar', 'text/html']) == 'foo/bar' assert accept.best_match([('foo/bar', 0.5), 'text/html']) == 'text/html' assert accept.best_match([('foo/bar', 0.5), ('text/html', 0.4)]) == 'foo/bar' assert_raises(ValueError, accept.best_match, ['text/*']) def test_best_match_with_one_lower_q(): accept = Accept('text/html, foo/bar;q=0.5') assert accept.best_match(['text/html', 'foo/bar']) == 'text/html' accept = Accept('text/html;q=0.5, foo/bar') assert accept.best_match(['text/html', 'foo/bar']) == 'foo/bar' def test_best_match_with_complex_q(): accept = Accept('text/html, foo/bar;q=0.55, baz/gort;q=0.59') assert accept.best_match(['text/html', 'foo/bar']) == 'text/html' accept = Accept('text/html;q=0.5, foo/bar;q=0.586, baz/gort;q=0.5966') assert "baz/gort;q=0.597" in str(accept) assert "foo/bar;q=0.586" in str(accept) assert "text/html;q=0.5" in str(accept) assert accept.best_match(['text/html', 'baz/gort']) == 'baz/gort' def test_accept_match(): for mask in ['*', 'text/html', 'TEXT/HTML']: assert 'text/html' in Accept(mask) assert 'text/html' not in Accept('foo/bar') def test_accept_match_lang(): for mask, lang in [ ('*', 'da'), ('da', 'DA'), ('en', 'en-gb'), ('en-gb', 'en-gb'), ('en-gb', 'en'), ('en-gb', 'en_GB'), ]: assert lang in AcceptLanguage(mask) for mask, lang in [ ('en-gb', 'en-us'), ('en-gb', 'fr-fr'), ('en-gb', 'fr'), ('en', 'fr-fr'), ]: assert lang not in AcceptLanguage(mask) # NilAccept tests def test_nil(): nilaccept = NilAccept() eq_(repr(nilaccept), ">" ) assert not nilaccept assert str(nilaccept) == '' assert nilaccept.quality('dummy') == 0 def test_nil_add(): nilaccept = NilAccept() accept = Accept('text/html') assert nilaccept + accept is accept new_accept = nilaccept + nilaccept assert isinstance(new_accept, accept.__class__) assert new_accept.header_value == '' new_accept = nilaccept + 'foo' assert isinstance(new_accept, accept.__class__) assert new_accept.header_value == 'foo' def test_nil_radd(): nilaccept = NilAccept() accept = Accept('text/html') assert isinstance('foo' + nilaccept, accept.__class__) assert ('foo' + nilaccept).header_value == 'foo' # How to test ``if isinstance(item, self.MasterClass): return item`` # under NilAccept.__radd__ ?? def test_nil_radd_masterclass(): # Is this "reaching into" __radd__ legit? nilaccept = NilAccept() accept = Accept('text/html') assert nilaccept.__radd__(accept) is accept def test_nil_contains(): nilaccept = NilAccept() assert 'anything' in nilaccept def test_nil_best_match(): nilaccept = NilAccept() assert nilaccept.best_match(['foo', 'bar']) == 'foo' assert nilaccept.best_match([('foo', 1), ('bar', 0.5)]) == 'foo' assert nilaccept.best_match([('foo', 0.5), ('bar', 1)]) == 'bar' assert nilaccept.best_match([('foo', 0.5), 'bar']) == 'bar' assert nilaccept.best_match([('foo', 0.5), 'bar'], default_match=True) == 'bar' assert nilaccept.best_match([('foo', 0.5), 'bar'], default_match=False) == 'bar' assert nilaccept.best_match([], default_match='fallback') == 'fallback' # NoAccept tests def test_noaccept_contains(): assert 'text/plain' not in NoAccept() # MIMEAccept tests def test_mime_init(): mimeaccept = MIMEAccept('image/jpg') assert mimeaccept._parsed == [('image/jpg', 1)] mimeaccept = MIMEAccept('image/png, image/jpg;q=0.5') assert mimeaccept._parsed == [('image/png', 1), ('image/jpg', 0.5)] mimeaccept = MIMEAccept('image, image/jpg;q=0.5') assert mimeaccept._parsed == [('image/jpg', 0.5)] mimeaccept = MIMEAccept('*/*') assert mimeaccept._parsed == [('*/*', 1)] mimeaccept = MIMEAccept('*/png') assert mimeaccept._parsed == [] mimeaccept = MIMEAccept('image/pn*') assert mimeaccept._parsed == [] mimeaccept = MIMEAccept('imag*/png') assert mimeaccept._parsed == [] mimeaccept = MIMEAccept('image/*') assert mimeaccept._parsed == [('image/*', 1)] def test_accept_html(): mimeaccept = MIMEAccept('image/jpg') assert not mimeaccept.accept_html() mimeaccept = MIMEAccept('image/jpg, text/html') assert mimeaccept.accept_html() def test_match(): mimeaccept = MIMEAccept('image/jpg') assert mimeaccept._match('image/jpg', 'image/jpg') assert mimeaccept._match('image/*', 'image/jpg') assert mimeaccept._match('*/*', 'image/jpg') assert not mimeaccept._match('text/html', 'image/jpg') assert_raises(ValueError, mimeaccept._match, 'image/jpg', '*/*') def test_accept_json(): mimeaccept = MIMEAccept('text/html, *; q=.2, */*; q=.2') assert mimeaccept.best_match(['application/json']) == 'application/json' def test_accept_mixedcase(): """3.7 Media Types [...] The type, subtype, and parameter attribute names are case- insensitive.""" mimeaccept = MIMEAccept('text/HtMl') assert mimeaccept.accept_html() def test_match_mixedcase(): mimeaccept = MIMEAccept('image/jpg; q=.2, Image/pNg; Q=.4, image/*; q=.05') assert mimeaccept.best_match(['Image/JpG']) == 'Image/JpG' assert mimeaccept.best_match(['image/Tiff']) == 'image/Tiff' assert mimeaccept.best_match(['image/Tiff', 'image/PnG', 'image/jpg']) == 'image/PnG' def test_match_uppercase_q(): """The relative-quality-factor "q" parameter is defined as an exact string in "14.1 Accept" BNF grammar""" mimeaccept = MIMEAccept('image/jpg; q=.4, Image/pNg; Q=.2, image/*; q=.05') assert mimeaccept._parsed == [('image/jpg', 0.4), ('image/png', 1), ('image/*', 0.05)] # property tests def test_accept_property_fget(): desc = accept_property('Accept-Charset', '14.2') req = Request.blank('/', environ={'envkey': 'envval'}) desc.fset(req, 'val') eq_(desc.fget(req).header_value, 'val') def test_accept_property_fget_nil(): desc = accept_property('Accept-Charset', '14.2') req = Request.blank('/') eq_(type(desc.fget(req)), NilAccept) def test_accept_property_fset(): desc = accept_property('Accept-Charset', '14.2') req = Request.blank('/', environ={'envkey': 'envval'}) desc.fset(req, 'baz') eq_(desc.fget(req).header_value, 'baz') def test_accept_property_fset_acceptclass(): req = Request.blank('/', environ={'envkey': 'envval'}) req.accept_charset = ['utf-8', 'latin-11'] eq_(req.accept_charset.header_value, 'utf-8, latin-11, iso-8859-1') def test_accept_property_fdel(): desc = accept_property('Accept-Charset', '14.2') req = Request.blank('/', environ={'envkey': 'envval'}) desc.fset(req, 'val') assert desc.fget(req).header_value == 'val' desc.fdel(req) eq_(type(desc.fget(req)), NilAccept) WebOb-1.3.1/tests/test_util.py0000644000175000017500000000273311710620612017037 0ustar chrismchrism00000000000000import unittest from webob.response import Response class Test_warn_deprecation(unittest.TestCase): def setUp(self): import warnings self.oldwarn = warnings.warn warnings.warn = self._warn self.warnings = [] def tearDown(self): import warnings warnings.warn = self.oldwarn del self.warnings def _callFUT(self, text, version, stacklevel): from webob.util import warn_deprecation return warn_deprecation(text, version, stacklevel) def _warn(self, text, type, stacklevel=1): self.warnings.append(locals()) def test_multidict_update_warning(self): # test warning when duplicate keys are passed r = Response() r.headers.update([ ('Set-Cookie', 'a=b'), ('Set-Cookie', 'x=y'), ]) self.assertEqual(len(self.warnings), 1) deprecation_warning = self.warnings[0] self.assertEqual(deprecation_warning['type'], UserWarning) assert 'Consider using .extend()' in deprecation_warning['text'] def test_multidict_update_warning_unnecessary(self): # no warning on normal operation r = Response() r.headers.update([('Set-Cookie', 'a=b')]) self.assertEqual(len(self.warnings), 0) def test_warn_deprecation(self): from webob import __version__ as v from webob.util import warn_deprecation self.assertRaises(DeprecationWarning, warn_deprecation, 'foo', v[:3], 1) WebOb-1.3.1/tests/conftest.py0000644000175000017500000000020211635430213016637 0ustar chrismchrism00000000000000import os, sys sys.path.insert(0, os.path.dirname(os.path.dirname(__file__))) import pkg_resources pkg_resources.require('WebOb') WebOb-1.3.1/tests/test_dec.py0000664000175000017500000001755612127135226016635 0ustar chrismchrism00000000000000import unittest from webob.request import Request from webob.response import Response from webob.dec import wsgify from webob.compat import bytes_ from webob.compat import text_ from webob.compat import PY3 class DecoratorTests(unittest.TestCase): def _testit(self, app, req): if isinstance(req, str): req = Request.blank(req) resp = req.get_response(app) return resp def test_wsgify(self): resp_str = 'hey, this is a test: %s' @wsgify def test_app(req): return bytes_(resp_str % req.url) resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, bytes_(resp_str % 'http://localhost/a%20url')) self.assertEqual(resp.content_length, 45) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') def test_wsgify_empty_repr(self): self.assertTrue('wsgify at' in repr(wsgify())) def test_wsgify_args(self): resp_str = b'hey hey my my' @wsgify(args=(resp_str,)) def test_app(req, strarg): return strarg resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, resp_str) self.assertEqual(resp.content_length, 13) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') def test_wsgify_kwargs(self): resp_str = b'hey hey my my' @wsgify(kwargs=dict(strarg=resp_str)) def test_app(req, strarg=''): return strarg resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, resp_str) self.assertEqual(resp.content_length, 13) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') def test_wsgify_raise_httpexception(self): from webob.exc import HTTPBadRequest @wsgify def test_app(req): raise HTTPBadRequest resp = self._testit(test_app, '/a url') self.assertTrue(resp.body.startswith(b'400 Bad Request')) self.assertEqual(resp.content_type, 'text/plain') self.assertEqual(resp.charset, 'UTF-8') def test_wsgify_no___get__(self): # use a class instance instead of a fn so we wrap something w/ # no __get__ class TestApp(object): def __call__(self, req): return 'nothing to see here' test_app = wsgify(TestApp()) resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, b'nothing to see here') self.assertTrue(test_app.__get__(test_app) is test_app) def test_wsgify_app_returns_unicode(self): def test_app(req): return text_('some text') test_app = wsgify(test_app) resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, b'some text') def test_wsgify_args_no_func(self): test_app = wsgify(None, args=(1,)) self.assertRaises(TypeError, self._testit, test_app, '/a url') def test_wsgify_wrong_sig(self): @wsgify def test_app(req): return 'What have you done for me lately?' req = dict() self.assertRaises(TypeError, test_app, req, 1, 2) self.assertRaises(TypeError, test_app, req, 1, key='word') def test_wsgify_none_response(self): @wsgify def test_app(req): return resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, b'') self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.content_length, 0) def test_wsgify_get(self): resp_str = b"What'choo talkin' about, Willis?" @wsgify def test_app(req): return Response(resp_str) resp = test_app.get('/url/path') self.assertEqual(resp.body, resp_str) def test_wsgify_post(self): post_dict = dict(speaker='Robin', words='Holy test coverage, Batman!') @wsgify def test_app(req): return Response('%s: %s' % (req.POST['speaker'], req.POST['words'])) resp = test_app.post('/url/path', post_dict) self.assertEqual(resp.body, bytes_('%s: %s' % (post_dict['speaker'], post_dict['words']))) def test_wsgify_request_method(self): resp_str = b'Nice body!' @wsgify def test_app(req): self.assertEqual(req.method, 'PUT') return Response(req.body) resp = test_app.request('/url/path', method='PUT', body=resp_str) self.assertEqual(resp.body, resp_str) self.assertEqual(resp.content_length, 10) self.assertEqual(resp.content_type, 'text/html') def test_wsgify_undecorated(self): def test_app(req): return Response('whoa') wrapped_test_app = wsgify(test_app) self.assertTrue(wrapped_test_app.undecorated is test_app) def test_wsgify_custom_request(self): resp_str = 'hey, this is a test: %s' class MyRequest(Request): pass @wsgify(RequestClass=MyRequest) def test_app(req): return bytes_(resp_str % req.url) resp = self._testit(test_app, '/a url') self.assertEqual(resp.body, bytes_(resp_str % 'http://localhost/a%20url')) self.assertEqual(resp.content_length, 45) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') def test_middleware(self): resp_str = "These are the vars: %s" @wsgify.middleware def set_urlvar(req, app, **vars): req.urlvars.update(vars) return app(req) from webob.dec import _MiddlewareFactory self.assertTrue(set_urlvar.__class__ is _MiddlewareFactory) r = repr(set_urlvar) self.assertTrue('set_urlvar' in r) @wsgify def show_vars(req): return resp_str % (sorted(req.urlvars.items())) show_vars2 = set_urlvar(show_vars, a=1, b=2) resp = self._testit(show_vars2, '/path') self.assertEqual(resp.body, bytes_(resp_str % "[('a', 1), ('b', 2)]")) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') self.assertEqual(resp.content_length, 40) def test_unbound_middleware(self): @wsgify def test_app(req): return Response('Say wha!?') unbound = wsgify.middleware(None, test_app, some='thing') from webob.dec import _UnboundMiddleware self.assertTrue(unbound.__class__ is _UnboundMiddleware) self.assertEqual(unbound.kw, dict(some='thing')) @unbound def middle(req, app, **kw): return app(req) self.assertTrue(middle.__class__ is wsgify) self.assertTrue('test_app' in repr(unbound)) def test_unbound_middleware_no_app(self): unbound = wsgify.middleware(None, None) from webob.dec import _UnboundMiddleware self.assertTrue(unbound.__class__ is _UnboundMiddleware) self.assertEqual(unbound.kw, dict()) def test_classapp(self): class HostMap(dict): @wsgify def __call__(self, req): return self[req.host.split(':')[0]] app = HostMap() app['example.com'] = Response('1') app['other.com'] = Response('2') resp = Request.blank('http://example.com/').get_response(wsgify(app)) self.assertEqual(resp.content_type, 'text/html') self.assertEqual(resp.charset, 'UTF-8') self.assertEqual(resp.content_length, 1) self.assertEqual(resp.body, b'1') def test_middleware_direct_call(self): @wsgify.middleware def mw(req, app): return 'foo' app = mw(Response()) self.assertEqual(app(Request.blank('/')), 'foo') WebOb-1.3.1/tests/test_in_wsgiref.py0000664000175000017500000001231412123261311020210 0ustar chrismchrism00000000000000import sys import logging import threading import random import socket import cgi from webob.request import Request from webob.response import Response from webob.compat import url_open from webob.compat import bytes_ from webob.compat import reraise from webob.compat import Queue from webob.compat import Empty from contextlib import contextmanager from nose.tools import assert_raises from nose.tools import eq_ as eq from wsgiref.simple_server import make_server from wsgiref.simple_server import WSGIRequestHandler from wsgiref.simple_server import WSGIServer from wsgiref.simple_server import ServerHandler log = logging.getLogger(__name__) def test_request_reading(): """ Test actual request/response cycle in the presence of Request.copy() and other methods that can potentially hang. """ with serve(_test_app_req_reading) as server: for key in _test_ops_req_read: resp = url_open(server.url+key, timeout=3) assert resp.read() == b"ok" def _test_app_req_reading(env, sr): req = Request(env) log.debug('starting test operation: %s', req.path_info) test_op = _test_ops_req_read[req.path_info] test_op(req) log.debug('done') r = Response("ok") return r(env, sr) _test_ops_req_read = { '/copy': lambda req: req.copy(), '/read-all': lambda req: req.body_file.read(), '/read-0': lambda req: req.body_file.read(0), '/make-seekable': lambda req: req.make_body_seekable() } # TODO: remove server logging for interrupted requests # TODO: test interrupted body directly def test_interrupted_request(): with serve(_test_app_req_interrupt) as server: for path in _test_ops_req_interrupt: _send_interrupted_req(server, path) try: res = _global_res.get(timeout=1) except Empty: raise AssertionError("Error during test %s", path) if res is not None: print("Error during test:", path) reraise(res) _global_res = Queue() def _test_app_req_interrupt(env, sr): target_cl = 100000 try: req = Request(env) cl = req.content_length if cl != target_cl: raise AssertionError( 'request.content_length is %s instead of %s' % (cl, target_cl)) op = _test_ops_req_interrupt[req.path_info] log.info("Running test: %s", req.path_info) assert_raises(IOError, op, req) except: _global_res.put(sys.exc_info()) else: _global_res.put(None) sr('200 OK', []) return [] def _req_int_cgi(req): assert req.body_file.read(0) == b'' #req.environ.setdefault('CONTENT_LENGTH', '0') d = cgi.FieldStorage( fp=req.body_file, environ=req.environ, ) def _req_int_readline(req): try: eq(req.body_file.readline(), b'a=b\n') except IOError: # too early to detect disconnect raise AssertionError("False disconnect alert") req.body_file.readline() _test_ops_req_interrupt = { '/copy': lambda req: req.copy(), '/read-body': lambda req: req.body, '/read-post': lambda req: req.POST, '/read-all': lambda req: req.body_file.read(), '/read-too-much': lambda req: req.body_file.read(1<<22), '/readline': _req_int_readline, '/readlines': lambda req: req.body_file.readlines(), '/read-cgi': _req_int_cgi, '/make-seekable': lambda req: req.make_body_seekable() } def _send_interrupted_req(server, path='/'): sock = socket.socket() sock.connect(('localhost', server.server_port)) f = sock.makefile('wb') f.write(bytes_(_interrupted_req % path)) f.flush() f.close() sock.close() _interrupted_req = ( "POST %s HTTP/1.0\r\n" "content-type: application/x-www-form-urlencoded\r\n" "content-length: 100000\r\n" "\r\n" ) _interrupted_req += 'a=b\nz='+'x'*10000 @contextmanager def serve(app): server = _make_test_server(app) try: #worker = threading.Thread(target=server.handle_request) worker = threading.Thread(target=server.serve_forever) worker.setDaemon(True) worker.start() server.url = "http://localhost:%d" % server.server_port log.debug("server started on %s", server.url) yield server finally: log.debug("shutting server down") server.shutdown() worker.join(1) if worker.isAlive(): log.warning('worker is hanged') else: log.debug("server stopped") class QuietHanlder(WSGIRequestHandler): def log_request(self, *args): pass ServerHandler.handle_error = lambda: None class QuietServer(WSGIServer): def handle_error(self, req, addr): pass def _make_test_server(app): maxport = ((1<<16)-1) # we'll make 3 attempts to find a free port for i in range(3, 0, -1): try: port = random.randint(maxport//2, maxport) server = make_server('localhost', port, app, server_class=QuietServer, handler_class=QuietHanlder ) server.timeout = 5 return server except: if i == 1: raise if __name__ == '__main__': #test_request_reading() test_interrupted_request() WebOb-1.3.1/tests/test_request.py0000664000175000017500000042306312251572473017573 0ustar chrismchrism00000000000000# -*- coding: utf-8 -*- import collections import sys import unittest import warnings from io import ( BytesIO, StringIO, ) from webob.compat import ( bytes_, native_, text_type, text_, PY3, ) class TestRequestCommon(unittest.TestCase): # unit tests of non-bytes-vs-text-specific methods of request object def _getTargetClass(self): from webob.request import Request return Request def _makeOne(self, *arg, **kw): cls = self._getTargetClass() return cls(*arg, **kw) def _blankOne(self, *arg, **kw): cls = self._getTargetClass() return cls.blank(*arg, **kw) def test_ctor_environ_getter_raises_WTF(self): self.assertRaises(TypeError, self._makeOne, {}, environ_getter=object()) def test_ctor_wo_environ_raises_WTF(self): self.assertRaises(TypeError, self._makeOne, None) def test_ctor_w_environ(self): environ = {} req = self._makeOne(environ) self.assertEqual(req.environ, environ) def test_ctor_w_non_utf8_charset(self): environ = {} self.assertRaises(DeprecationWarning, self._makeOne, environ, charset='latin-1') def test_scheme(self): environ = {'wsgi.url_scheme': 'something:', } req = self._makeOne(environ) self.assertEqual(req.scheme, 'something:') def test_body_file_getter(self): body = b'input' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': len(body), 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) self.assertTrue(req.body_file is not INPUT) def test_body_file_getter_seekable(self): body = b'input' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': len(body), 'REQUEST_METHOD': 'POST', 'webob.is_body_seekable': True, } req = self._makeOne(environ) self.assertTrue(req.body_file is INPUT) def test_body_file_getter_cache(self): body = b'input' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': len(body), 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) self.assertTrue(req.body_file is req.body_file) def test_body_file_getter_unreadable(self): body = b'input' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'REQUEST_METHOD': 'FOO'} req = self._makeOne(environ) assert req.body_file_raw is INPUT assert req.body_file is not INPUT assert req.body_file.read() == b'' def test_body_file_setter_w_bytes(self): req = self._blankOne('/') self.assertRaises(DeprecationWarning, setattr, req, 'body_file', b'foo') def test_body_file_setter_non_bytes(self): BEFORE = BytesIO(b'before') AFTER = BytesIO(b'after') environ = {'wsgi.input': BEFORE, 'CONTENT_LENGTH': len('before'), 'REQUEST_METHOD': 'POST' } req = self._makeOne(environ) req.body_file = AFTER self.assertTrue(req.body_file is AFTER) self.assertEqual(req.content_length, None) def test_body_file_deleter(self): body = b'input' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': len(body), 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) del req.body_file self.assertEqual(req.body_file.getvalue(), b'') self.assertEqual(req.content_length, 0) def test_body_file_raw(self): INPUT = BytesIO(b'input') environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': len('input'), 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) self.assertTrue(req.body_file_raw is INPUT) def test_body_file_seekable_input_not_seekable(self): data = b'input' INPUT = BytesIO(data) INPUT.seek(1, 0) # consume environ = {'wsgi.input': INPUT, 'webob.is_body_seekable': False, 'CONTENT_LENGTH': len(data)-1, 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) seekable = req.body_file_seekable self.assertTrue(seekable is not INPUT) self.assertEqual(seekable.getvalue(), b'nput') def test_body_file_seekable_input_is_seekable(self): INPUT = BytesIO(b'input') INPUT.seek(1, 0) # consume environ = {'wsgi.input': INPUT, 'webob.is_body_seekable': True, 'CONTENT_LENGTH': len('input')-1, 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) seekable = req.body_file_seekable self.assertTrue(seekable is INPUT) def test_urlvars_getter_w_paste_key(self): environ = {'paste.urlvars': {'foo': 'bar'}, } req = self._makeOne(environ) self.assertEqual(req.urlvars, {'foo': 'bar'}) def test_urlvars_getter_w_wsgiorg_key(self): environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}), } req = self._makeOne(environ) self.assertEqual(req.urlvars, {'foo': 'bar'}) def test_urlvars_getter_wo_keys(self): environ = {} req = self._makeOne(environ) self.assertEqual(req.urlvars, {}) self.assertEqual(environ['wsgiorg.routing_args'], ((), {})) def test_urlvars_setter_w_paste_key(self): environ = {'paste.urlvars': {'foo': 'bar'}, } req = self._makeOne(environ) req.urlvars = {'baz': 'bam'} self.assertEqual(req.urlvars, {'baz': 'bam'}) self.assertEqual(environ['paste.urlvars'], {'baz': 'bam'}) self.assertTrue('wsgiorg.routing_args' not in environ) def test_urlvars_setter_w_wsgiorg_key(self): environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}), 'paste.urlvars': {'qux': 'spam'}, } req = self._makeOne(environ) req.urlvars = {'baz': 'bam'} self.assertEqual(req.urlvars, {'baz': 'bam'}) self.assertEqual(environ['wsgiorg.routing_args'], ((), {'baz': 'bam'})) self.assertTrue('paste.urlvars' not in environ) def test_urlvars_setter_wo_keys(self): environ = {} req = self._makeOne(environ) req.urlvars = {'baz': 'bam'} self.assertEqual(req.urlvars, {'baz': 'bam'}) self.assertEqual(environ['wsgiorg.routing_args'], ((), {'baz': 'bam'})) self.assertTrue('paste.urlvars' not in environ) def test_urlvars_deleter_w_paste_key(self): environ = {'paste.urlvars': {'foo': 'bar'}, } req = self._makeOne(environ) del req.urlvars self.assertEqual(req.urlvars, {}) self.assertTrue('paste.urlvars' not in environ) self.assertEqual(environ['wsgiorg.routing_args'], ((), {})) def test_urlvars_deleter_w_wsgiorg_key_non_empty_tuple(self): environ = {'wsgiorg.routing_args': (('a', 'b'), {'foo': 'bar'}), 'paste.urlvars': {'qux': 'spam'}, } req = self._makeOne(environ) del req.urlvars self.assertEqual(req.urlvars, {}) self.assertEqual(environ['wsgiorg.routing_args'], (('a', 'b'), {})) self.assertTrue('paste.urlvars' not in environ) def test_urlvars_deleter_w_wsgiorg_key_empty_tuple(self): environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}), 'paste.urlvars': {'qux': 'spam'}, } req = self._makeOne(environ) del req.urlvars self.assertEqual(req.urlvars, {}) self.assertEqual(environ['wsgiorg.routing_args'], ((), {})) self.assertTrue('paste.urlvars' not in environ) def test_urlvars_deleter_wo_keys(self): environ = {} req = self._makeOne(environ) del req.urlvars self.assertEqual(req.urlvars, {}) self.assertEqual(environ['wsgiorg.routing_args'], ((), {})) self.assertTrue('paste.urlvars' not in environ) def test_urlargs_getter_w_paste_key(self): environ = {'paste.urlvars': {'foo': 'bar'}, } req = self._makeOne(environ) self.assertEqual(req.urlargs, ()) def test_urlargs_getter_w_wsgiorg_key(self): environ = {'wsgiorg.routing_args': (('a', 'b'), {'foo': 'bar'}), } req = self._makeOne(environ) self.assertEqual(req.urlargs, ('a', 'b')) def test_urlargs_getter_wo_keys(self): environ = {} req = self._makeOne(environ) self.assertEqual(req.urlargs, ()) self.assertTrue('wsgiorg.routing_args' not in environ) def test_urlargs_setter_w_paste_key(self): environ = {'paste.urlvars': {'foo': 'bar'}, } req = self._makeOne(environ) req.urlargs = ('a', 'b') self.assertEqual(req.urlargs, ('a', 'b')) self.assertEqual(environ['wsgiorg.routing_args'], (('a', 'b'), {'foo': 'bar'})) self.assertTrue('paste.urlvars' not in environ) def test_urlargs_setter_w_wsgiorg_key(self): environ = {'wsgiorg.routing_args': ((), {'foo': 'bar'}), } req = self._makeOne(environ) req.urlargs = ('a', 'b') self.assertEqual(req.urlargs, ('a', 'b')) self.assertEqual(environ['wsgiorg.routing_args'], (('a', 'b'), {'foo': 'bar'})) def test_urlargs_setter_wo_keys(self): environ = {} req = self._makeOne(environ) req.urlargs = ('a', 'b') self.assertEqual(req.urlargs, ('a', 'b')) self.assertEqual(environ['wsgiorg.routing_args'], (('a', 'b'), {})) self.assertTrue('paste.urlvars' not in environ) def test_urlargs_deleter_w_wsgiorg_key(self): environ = {'wsgiorg.routing_args': (('a', 'b'), {'foo': 'bar'}), } req = self._makeOne(environ) del req.urlargs self.assertEqual(req.urlargs, ()) self.assertEqual(environ['wsgiorg.routing_args'], ((), {'foo': 'bar'})) def test_urlargs_deleter_w_wsgiorg_key_empty(self): environ = {'wsgiorg.routing_args': ((), {}), } req = self._makeOne(environ) del req.urlargs self.assertEqual(req.urlargs, ()) self.assertTrue('paste.urlvars' not in environ) self.assertTrue('wsgiorg.routing_args' not in environ) def test_urlargs_deleter_wo_keys(self): environ = {} req = self._makeOne(environ) del req.urlargs self.assertEqual(req.urlargs, ()) self.assertTrue('paste.urlvars' not in environ) self.assertTrue('wsgiorg.routing_args' not in environ) def test_cookies_empty_environ(self): req = self._makeOne({}) self.assertEqual(req.cookies, {}) def test_cookies_is_mutable(self): req = self._makeOne({}) cookies = req.cookies cookies['a'] = '1' self.assertEqual(req.cookies['a'], '1') def test_cookies_w_webob_parsed_cookies_matching_source(self): environ = { 'HTTP_COOKIE': 'a=b', 'webob._parsed_cookies': ('a=b', {'a': 'b'}), } req = self._makeOne(environ) self.assertEqual(req.cookies, {'a': 'b'}) def test_cookies_w_webob_parsed_cookies_mismatched_source(self): environ = { 'HTTP_COOKIE': 'a=b', 'webob._parsed_cookies': ('a=b;c=d', {'a': 'b', 'c': 'd'}), } req = self._makeOne(environ) self.assertEqual(req.cookies, {'a': 'b'}) def test_set_cookies(self): environ = { 'HTTP_COOKIE': 'a=b', } req = self._makeOne(environ) req.cookies = {'a':'1', 'b': '2'} self.assertEqual(req.cookies, {'a': '1', 'b':'2'}) rcookies = [x.strip() for x in environ['HTTP_COOKIE'].split(';')] self.assertEqual(sorted(rcookies), ['a=1', 'b=2']) # body def test_body_getter(self): INPUT = BytesIO(b'input') environ = {'wsgi.input': INPUT, 'webob.is_body_seekable': True, 'CONTENT_LENGTH': len('input'), 'REQUEST_METHOD': 'POST' } req = self._makeOne(environ) self.assertEqual(req.body, b'input') self.assertEqual(req.content_length, len(b'input')) def test_body_setter_None(self): INPUT = BytesIO(b'input') environ = {'wsgi.input': INPUT, 'webob.is_body_seekable': True, 'CONTENT_LENGTH': len(b'input'), 'REQUEST_METHOD': 'POST' } req = self._makeOne(environ) req.body = None self.assertEqual(req.body, b'') self.assertEqual(req.content_length, 0) self.assertTrue(req.is_body_seekable) def test_body_setter_non_string_raises(self): req = self._makeOne({}) def _test(): req.body = object() self.assertRaises(TypeError, _test) def test_body_setter_value(self): BEFORE = BytesIO(b'before') environ = {'wsgi.input': BEFORE, 'webob.is_body_seekable': True, 'CONTENT_LENGTH': len('before'), 'REQUEST_METHOD': 'POST' } req = self._makeOne(environ) req.body = b'after' self.assertEqual(req.body, b'after') self.assertEqual(req.content_length, len(b'after')) self.assertTrue(req.is_body_seekable) def test_body_deleter_None(self): data = b'input' INPUT = BytesIO(data) environ = {'wsgi.input': INPUT, 'webob.is_body_seekable': True, 'CONTENT_LENGTH': len(data), 'REQUEST_METHOD': 'POST', } req = self._makeOne(environ) del req.body self.assertEqual(req.body, b'') self.assertEqual(req.content_length, 0) self.assertTrue(req.is_body_seekable) # JSON def test_json_body(self): body = b'{"a":1}' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': str(len(body))} req = self._makeOne(environ) self.assertEqual(req.json, {"a": 1}) self.assertEqual(req.json_body, {"a": 1}) req.json = {"b": 2} self.assertEqual(req.body, b'{"b":2}') del req.json self.assertEqual(req.body, b'') # .text def test_text_body(self): body = b'test' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': str(len(body))} req = self._makeOne(environ) self.assertEqual(req.body, b'test') self.assertEqual(req.text, 'test') req.text = text_('\u1000') self.assertEqual(req.body, '\u1000'.encode(req.charset)) del req.text self.assertEqual(req.body, b'') def set_bad_text(): req.text = 1 self.assertRaises(TypeError, set_bad_text) def test__text_get_without_charset(self): body = b'test' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': str(len(body))} req = self._makeOne(environ) req._charset = '' self.assertRaises(AttributeError, getattr, req, 'text') def test__text_set_without_charset(self): body = b'test' INPUT = BytesIO(body) environ = {'wsgi.input': INPUT, 'CONTENT_LENGTH': str(len(body))} req = self._makeOne(environ) req._charset = '' self.assertRaises(AttributeError, setattr, req, 'text', 'abc') # POST def test_POST_not_POST_or_PUT(self): from webob.multidict import NoVars environ = {'REQUEST_METHOD': 'GET', } req = self._makeOne(environ) result = req.POST self.assertTrue(isinstance(result, NoVars)) self.assertTrue(result.reason.startswith('Not a form request')) def test_POST_existing_cache_hit(self): data = b'input' INPUT = BytesIO(data) environ = {'wsgi.input': INPUT, 'REQUEST_METHOD': 'POST', 'webob._parsed_post_vars': ({'foo': 'bar'}, INPUT), } req = self._makeOne(environ) result = req.POST self.assertEqual(result, {'foo': 'bar'}) def test_PUT_missing_content_type(self): from webob.multidict import NoVars data = b'input' INPUT = BytesIO(data) environ = {'wsgi.input': INPUT, 'REQUEST_METHOD': 'PUT', } req = self._makeOne(environ) result = req.POST self.assertTrue(isinstance(result, NoVars)) self.assertTrue(result.reason.startswith( 'Not an HTML form submission')) def test_POST_missing_content_type(self): data = b'var1=value1&var2=value2&rep=1&rep=2' INPUT = BytesIO(data) environ = {'wsgi.input': INPUT, 'REQUEST_METHOD': 'POST', 'CONTENT_LENGTH':len(data), 'webob.is_body_seekable': True, } req = self._makeOne(environ) result = req.POST self.assertEqual(result['var1'], 'value1') def test_PUT_bad_content_type(self): from webob.multidict import NoVars data = b'input' INPUT = BytesIO(data) environ = {'wsgi.input': INPUT, 'REQUEST_METHOD': 'PUT', 'CONTENT_TYPE': 'text/plain', } req = self._makeOne(environ) result = req.POST self.assertTrue(isinstance(result, NoVars)) self.assertTrue(result.reason.startswith( 'Not an HTML form submission')) def test_POST_multipart(self): BODY_TEXT = ( b'------------------------------deb95b63e42a\n' b'Content-Disposition: form-data; name="foo"\n' b'\n' b'foo\n' b'------------------------------deb95b63e42a\n' b'Content-Disposition: form-data; name="bar"; filename="bar.txt"\n' b'Content-type: application/octet-stream\n' b'\n' b'these are the contents of the file "bar.txt"\n' b'\n' b'------------------------------deb95b63e42a--\n') INPUT = BytesIO(BODY_TEXT) environ = {'wsgi.input': INPUT, 'webob.is_body_seekable': True, 'REQUEST_METHOD': 'POST', 'CONTENT_TYPE': 'multipart/form-data; ' 'boundary=----------------------------deb95b63e42a', 'CONTENT_LENGTH': len(BODY_TEXT), } req = self._makeOne(environ) result = req.POST self.assertEqual(result['foo'], 'foo') bar = result['bar'] self.assertEqual(bar.name, 'bar') self.assertEqual(bar.filename, 'bar.txt') self.assertEqual(bar.file.read(), b'these are the contents of the file "bar.txt"\n') # GET def test_GET_reflects_query_string(self): environ = { 'QUERY_STRING': 'foo=123', } req = self._makeOne(environ) result = req.GET self.assertEqual(result, {'foo': '123'}) req.query_string = 'foo=456' result = req.GET self.assertEqual(result, {'foo': '456'}) req.query_string = '' result = req.GET self.assertEqual(result, {}) def test_GET_updates_query_string(self): req = self._makeOne({}) result = req.query_string self.assertEqual(result, '') req.GET['foo'] = '123' result = req.query_string self.assertEqual(result, 'foo=123') del req.GET['foo'] result = req.query_string self.assertEqual(result, '') # cookies def test_cookies_wo_webob_parsed_cookies(self): environ = { 'HTTP_COOKIE': 'a=b', } req = self._blankOne('/', environ) self.assertEqual(req.cookies, {'a': 'b'}) # copy def test_copy_get(self): environ = { 'HTTP_COOKIE': 'a=b', } req = self._blankOne('/', environ) clone = req.copy_get() for k, v in req.environ.items(): if k in ('CONTENT_LENGTH', 'webob.is_body_seekable'): self.assertTrue(k not in clone.environ) elif k == 'wsgi.input': self.assertTrue(clone.environ[k] is not v) else: self.assertEqual(clone.environ[k], v) def test_remove_conditional_headers_accept_encoding(self): req = self._blankOne('/') req.accept_encoding='gzip,deflate' req.remove_conditional_headers() self.assertEqual(bool(req.accept_encoding), False) def test_remove_conditional_headers_if_modified_since(self): from webob.datetime_utils import UTC from datetime import datetime req = self._blankOne('/') req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) req.remove_conditional_headers() self.assertEqual(req.if_modified_since, None) def test_remove_conditional_headers_if_none_match(self): req = self._blankOne('/') req.if_none_match = 'foo' assert req.if_none_match req.remove_conditional_headers() assert not req.if_none_match def test_remove_conditional_headers_if_range(self): req = self._blankOne('/') req.if_range = 'foo, bar' req.remove_conditional_headers() self.assertEqual(bool(req.if_range), False) def test_remove_conditional_headers_range(self): req = self._blankOne('/') req.range = 'bytes=0-100' req.remove_conditional_headers() self.assertEqual(req.range, None) def test_is_body_readable_POST(self): req = self._blankOne('/', environ={'REQUEST_METHOD':'POST'}) self.assertTrue(req.is_body_readable) def test_is_body_readable_PATCH(self): req = self._blankOne('/', environ={'REQUEST_METHOD':'PATCH'}) self.assertTrue(req.is_body_readable) def test_is_body_readable_GET(self): req = self._blankOne('/', environ={'REQUEST_METHOD':'GET'}) self.assertFalse(req.is_body_readable) def test_is_body_readable_unknown_method_and_content_length(self): req = self._blankOne('/', environ={'REQUEST_METHOD':'WTF'}) req.content_length = 10 self.assertTrue(req.is_body_readable) def test_is_body_readable_special_flag(self): req = self._blankOne('/', environ={'REQUEST_METHOD':'WTF', 'webob.is_body_readable': True}) self.assertTrue(req.is_body_readable) # is_body_seekable # make_body_seekable # copy_body # make_tempfile # remove_conditional_headers # accept # accept_charset # accept_encoding # accept_language # authorization # cache_control def test_cache_control_reflects_environ(self): environ = { 'HTTP_CACHE_CONTROL': 'max-age=5', } req = self._makeOne(environ) result = req.cache_control self.assertEqual(result.properties, {'max-age': 5}) req.environ.update(HTTP_CACHE_CONTROL='max-age=10') result = req.cache_control self.assertEqual(result.properties, {'max-age': 10}) req.environ.update(HTTP_CACHE_CONTROL='') result = req.cache_control self.assertEqual(result.properties, {}) def test_cache_control_updates_environ(self): environ = {} req = self._makeOne(environ) req.cache_control.max_age = 5 result = req.environ['HTTP_CACHE_CONTROL'] self.assertEqual(result, 'max-age=5') req.cache_control.max_age = 10 result = req.environ['HTTP_CACHE_CONTROL'] self.assertEqual(result, 'max-age=10') req.cache_control = None result = req.environ['HTTP_CACHE_CONTROL'] self.assertEqual(result, '') del req.cache_control self.assertTrue('HTTP_CACHE_CONTROL' not in req.environ) def test_cache_control_set_dict(self): environ = {} req = self._makeOne(environ) req.cache_control = {'max-age': 5} result = req.cache_control self.assertEqual(result.max_age, 5) def test_cache_control_set_object(self): from webob.cachecontrol import CacheControl environ = {} req = self._makeOne(environ) req.cache_control = CacheControl({'max-age': 5}, type='request') result = req.cache_control self.assertEqual(result.max_age, 5) def test_cache_control_gets_cached(self): environ = {} req = self._makeOne(environ) self.assertTrue(req.cache_control is req.cache_control) #if_match #if_none_match #date #if_modified_since #if_unmodified_since #if_range #max_forwards #pragma #range #referer #referrer #user_agent #__repr__ #__str__ #from_file #call_application def test_call_application_calls_application(self): environ = {} req = self._makeOne(environ) def application(environ, start_response): start_response('200 OK', [('content-type', 'text/plain')]) return ['...\n'] status, headers, output = req.call_application(application) self.assertEqual(status, '200 OK') self.assertEqual(headers, [('content-type', 'text/plain')]) self.assertEqual(''.join(output), '...\n') def test_call_application_provides_write(self): environ = {} req = self._makeOne(environ) def application(environ, start_response): write = start_response('200 OK', [('content-type', 'text/plain')]) write('...\n') return [] status, headers, output = req.call_application(application) self.assertEqual(status, '200 OK') self.assertEqual(headers, [('content-type', 'text/plain')]) self.assertEqual(''.join(output), '...\n') def test_call_application_closes_iterable_when_mixed_w_write_calls(self): environ = { 'test._call_application_called_close': False } req = self._makeOne(environ) def application(environ, start_response): write = start_response('200 OK', [('content-type', 'text/plain')]) class AppIter(object): def __iter__(self): yield '...\n' def close(self): environ['test._call_application_called_close'] = True write('...\n') return AppIter() status, headers, output = req.call_application(application) self.assertEqual(''.join(output), '...\n...\n') self.assertEqual(environ['test._call_application_called_close'], True) def test_call_application_raises_exc_info(self): environ = {} req = self._makeOne(environ) def application(environ, start_response): try: raise RuntimeError('OH NOES') except: exc_info = sys.exc_info() start_response('200 OK', [('content-type', 'text/plain')], exc_info) return ['...\n'] self.assertRaises(RuntimeError, req.call_application, application) def test_call_application_returns_exc_info(self): environ = {} req = self._makeOne(environ) def application(environ, start_response): try: raise RuntimeError('OH NOES') except: exc_info = sys.exc_info() start_response('200 OK', [('content-type', 'text/plain')], exc_info) return ['...\n'] status, headers, output, exc_info = req.call_application( application, True) self.assertEqual(status, '200 OK') self.assertEqual(headers, [('content-type', 'text/plain')]) self.assertEqual(''.join(output), '...\n') self.assertEqual(exc_info[0], RuntimeError) #get_response def test_blank__method_subtitution(self): request = self._blankOne('/', environ={'REQUEST_METHOD': 'PUT'}) self.assertEqual(request.method, 'PUT') request = self._blankOne( '/', environ={'REQUEST_METHOD': 'PUT'}, POST={}) self.assertEqual(request.method, 'PUT') request = self._blankOne( '/', environ={'REQUEST_METHOD': 'HEAD'}, POST={}) self.assertEqual(request.method, 'POST') def test_blank__ctype_in_env(self): request = self._blankOne( '/', environ={'CONTENT_TYPE': 'application/json'}) self.assertEqual(request.content_type, 'application/json') self.assertEqual(request.method, 'GET') request = self._blankOne( '/', environ={'CONTENT_TYPE': 'application/json'}, POST='') self.assertEqual(request.content_type, 'application/json') self.assertEqual(request.method, 'POST') def test_blank__ctype_in_headers(self): request = self._blankOne( '/', headers={'Content-type': 'application/json'}) self.assertEqual(request.content_type, 'application/json') self.assertEqual(request.method, 'GET') request = self._blankOne( '/', headers={'Content-Type': 'application/json'}, POST='') self.assertEqual(request.content_type, 'application/json') self.assertEqual(request.method, 'POST') def test_blank__ctype_as_kw(self): request = self._blankOne('/', content_type='application/json') self.assertEqual(request.content_type, 'application/json') self.assertEqual(request.method, 'GET') request = self._blankOne('/', content_type='application/json', POST='') self.assertEqual(request.content_type, 'application/json') self.assertEqual(request.method, 'POST') def test_blank__str_post_data_for_unsupported_ctype(self): self.assertRaises(ValueError, self._blankOne, '/', content_type='application/json', POST={}) def test_blank__post_urlencoded(self): from webob.multidict import MultiDict POST = MultiDict() POST["first"] = 1 POST["second"] = 2 request = self._blankOne('/', POST=POST) self.assertEqual(request.method, 'POST') self.assertEqual(request.content_type, 'application/x-www-form-urlencoded') self.assertEqual(request.body, b'first=1&second=2') self.assertEqual(request.content_length, 16) def test_blank__post_multipart(self): from webob.multidict import MultiDict POST = MultiDict() POST["first"] = "1" POST["second"] = "2" request = self._blankOne('/', POST=POST, content_type='multipart/form-data; ' 'boundary=boundary') self.assertEqual(request.method, 'POST') self.assertEqual(request.content_type, 'multipart/form-data') expected = ( b'--boundary\r\n' b'Content-Disposition: form-data; name="first"\r\n\r\n' b'1\r\n' b'--boundary\r\n' b'Content-Disposition: form-data; name="second"\r\n\r\n' b'2\r\n' b'--boundary--') self.assertEqual(request.body, expected) self.assertEqual(request.content_length, 139) def test_blank__post_files(self): import cgi from webob.request import _get_multipart_boundary from webob.multidict import MultiDict POST = MultiDict() POST["first"] = ('filename1', BytesIO(b'1')) POST["second"] = ('filename2', '2') POST["third"] = "3" request = self._blankOne('/', POST=POST) self.assertEqual(request.method, 'POST') self.assertEqual(request.content_type, 'multipart/form-data') boundary = bytes_( _get_multipart_boundary(request.headers['content-type'])) body_norm = request.body.replace(boundary, b'boundary') expected = ( b'--boundary\r\n' b'Content-Disposition: form-data; name="first"; ' b'filename="filename1"\r\n\r\n' b'1\r\n' b'--boundary\r\n' b'Content-Disposition: form-data; name="second"; ' b'filename="filename2"\r\n\r\n' b'2\r\n' b'--boundary\r\n' b'Content-Disposition: form-data; name="third"\r\n\r\n' b'3\r\n' b'--boundary--' ) self.assertEqual(body_norm, expected) self.assertEqual(request.content_length, 294) self.assertTrue(isinstance(request.POST['first'], cgi.FieldStorage)) self.assertTrue(isinstance(request.POST['second'], cgi.FieldStorage)) self.assertEqual(request.POST['first'].value, b'1') self.assertEqual(request.POST['second'].value, b'2') self.assertEqual(request.POST['third'], '3') def test_blank__post_file_w_wrong_ctype(self): self.assertRaises( ValueError, self._blankOne, '/', POST={'first':('filename1', '1')}, content_type='application/x-www-form-urlencoded') #from_bytes def test_from_bytes_extra_data(self): _test_req_copy = _test_req.replace( b'Content-Type', b'Content-Length: 337\r\nContent-Type') cls = self._getTargetClass() self.assertRaises(ValueError, cls.from_bytes, _test_req_copy+b'EXTRA!') #as_bytes def test_as_bytes_skip_body(self): cls = self._getTargetClass() req = cls.from_bytes(_test_req) body = req.as_bytes(skip_body=True) self.assertEqual(body.count(b'\r\n\r\n'), 0) self.assertEqual(req.as_bytes(skip_body=337), req.as_bytes()) body = req.as_bytes(337-1).split(b'\r\n\r\n', 1)[1] self.assertEqual(body, b'') def test_as_string_skip_body(self): with warnings.catch_warnings(record=True): cls = self._getTargetClass() req = cls.from_string(_test_req) body = req.as_string(skip_body=True) self.assertEqual(body.count(b'\r\n\r\n'), 0) self.assertEqual(req.as_string(skip_body=337), req.as_string()) body = req.as_string(337-1).split(b'\r\n\r\n', 1)[1] self.assertEqual(body, b'') class TestBaseRequest(unittest.TestCase): # tests of methods of a base request which are encoding-specific def _getTargetClass(self): from webob.request import BaseRequest return BaseRequest def _makeOne(self, *arg, **kw): cls = self._getTargetClass() return cls(*arg, **kw) def _blankOne(self, *arg, **kw): cls = self._getTargetClass() return cls.blank(*arg, **kw) def test_method(self): environ = {'REQUEST_METHOD': 'OPTIONS', } req = self._makeOne(environ) result = req.method self.assertEqual(result.__class__, str) self.assertEqual(result, 'OPTIONS') def test_http_version(self): environ = {'SERVER_PROTOCOL': '1.1', } req = self._makeOne(environ) result = req.http_version self.assertEqual(result, '1.1') def test_script_name(self): environ = {'SCRIPT_NAME': '/script', } req = self._makeOne(environ) self.assertEqual(req.script_name, '/script') def test_path_info(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) self.assertEqual(req.path_info, '/path/info') def test_content_length_getter(self): environ = {'CONTENT_LENGTH': '1234', } req = self._makeOne(environ) self.assertEqual(req.content_length, 1234) def test_content_length_setter_w_str(self): environ = {'CONTENT_LENGTH': '1234', } req = self._makeOne(environ) req.content_length = '3456' self.assertEqual(req.content_length, 3456) def test_remote_user(self): environ = {'REMOTE_USER': 'phred', } req = self._makeOne(environ) self.assertEqual(req.remote_user, 'phred') def test_remote_addr(self): environ = {'REMOTE_ADDR': '1.2.3.4', } req = self._makeOne(environ) self.assertEqual(req.remote_addr, '1.2.3.4') def test_query_string(self): environ = {'QUERY_STRING': 'foo=bar&baz=bam', } req = self._makeOne(environ) self.assertEqual(req.query_string, 'foo=bar&baz=bam') def test_server_name(self): environ = {'SERVER_NAME': 'somehost.tld', } req = self._makeOne(environ) self.assertEqual(req.server_name, 'somehost.tld') def test_server_port_getter(self): environ = {'SERVER_PORT': '6666', } req = self._makeOne(environ) self.assertEqual(req.server_port, 6666) def test_server_port_setter_with_string(self): environ = {'SERVER_PORT': '6666', } req = self._makeOne(environ) req.server_port = '6667' self.assertEqual(req.server_port, 6667) def test_uscript_name(self): environ = {'SCRIPT_NAME': '/script', } req = self._makeOne(environ) self.assertTrue(isinstance(req.uscript_name, text_type)) self.assertEqual(req.uscript_name, '/script') def test_upath_info(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) self.assertTrue(isinstance(req.upath_info, text_type)) self.assertEqual(req.upath_info, '/path/info') def test_upath_info_set_unicode(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) req.upath_info = text_('/another') self.assertTrue(isinstance(req.upath_info, text_type)) self.assertEqual(req.upath_info, '/another') def test_content_type_getter_no_parameters(self): environ = {'CONTENT_TYPE': 'application/xml+foobar', } req = self._makeOne(environ) self.assertEqual(req.content_type, 'application/xml+foobar') def test_content_type_getter_w_parameters(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) self.assertEqual(req.content_type, 'application/xml+foobar') def test_content_type_setter_w_None(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) req.content_type = None self.assertEqual(req.content_type, '') self.assertTrue('CONTENT_TYPE' not in environ) def test_content_type_setter_existing_paramter_no_new_paramter(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) req.content_type = 'text/xml' self.assertEqual(req.content_type, 'text/xml') self.assertEqual(environ['CONTENT_TYPE'], 'text/xml;charset="utf8"') def test_content_type_deleter_clears_environ_value(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) del req.content_type self.assertEqual(req.content_type, '') self.assertTrue('CONTENT_TYPE' not in environ) def test_content_type_deleter_no_environ_value(self): environ = {} req = self._makeOne(environ) del req.content_type self.assertEqual(req.content_type, '') self.assertTrue('CONTENT_TYPE' not in environ) def test_headers_getter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) headers = req.headers self.assertEqual(headers, {'Content-Type': CONTENT_TYPE, 'Content-Length': '123'}) def test_headers_setter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) req.headers = {'Qux': 'Spam'} self.assertEqual(req.headers, {'Qux': 'Spam'}) self.assertEqual(environ, {'HTTP_QUX': 'Spam'}) def test_no_headers_deleter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) def _test(): del req.headers self.assertRaises(AttributeError, _test) def test_client_addr_xff_singleval(self): environ = { 'HTTP_X_FORWARDED_FOR': '192.168.1.1', } req = self._makeOne(environ) self.assertEqual(req.client_addr, '192.168.1.1') def test_client_addr_xff_multival(self): environ = { 'HTTP_X_FORWARDED_FOR': '192.168.1.1, 192.168.1.2', } req = self._makeOne(environ) self.assertEqual(req.client_addr, '192.168.1.1') def test_client_addr_prefers_xff(self): environ = {'REMOTE_ADDR': '192.168.1.2', 'HTTP_X_FORWARDED_FOR': '192.168.1.1', } req = self._makeOne(environ) self.assertEqual(req.client_addr, '192.168.1.1') def test_client_addr_no_xff(self): environ = {'REMOTE_ADDR': '192.168.1.2', } req = self._makeOne(environ) self.assertEqual(req.client_addr, '192.168.1.2') def test_client_addr_no_xff_no_remote_addr(self): environ = {} req = self._makeOne(environ) self.assertEqual(req.client_addr, None) def test_host_port_w_http_host_and_no_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) self.assertEqual(req.host_port, '80') def test_host_port_w_http_host_and_standard_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:80', } req = self._makeOne(environ) self.assertEqual(req.host_port, '80') def test_host_port_w_http_host_and_oddball_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) self.assertEqual(req.host_port, '8888') def test_host_port_w_http_host_https_and_no_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) self.assertEqual(req.host_port, '443') def test_host_port_w_http_host_https_and_standard_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:443', } req = self._makeOne(environ) self.assertEqual(req.host_port, '443') def test_host_port_w_http_host_https_and_oddball_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) self.assertEqual(req.host_port, '8888') def test_host_port_wo_http_host(self): environ = {'wsgi.url_scheme': 'https', 'SERVER_PORT': '4333', } req = self._makeOne(environ) self.assertEqual(req.host_port, '4333') def test_host_url_w_http_host_and_no_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'http://example.com') def test_host_url_w_http_host_and_standard_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:80', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'http://example.com') def test_host_url_w_http_host_and_oddball_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'http://example.com:8888') def test_host_url_w_http_host_https_and_no_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'https://example.com') def test_host_url_w_http_host_https_and_standard_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:443', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'https://example.com') def test_host_url_w_http_host_https_and_oddball_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:4333', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'https://example.com:4333') def test_host_url_wo_http_host(self): environ = {'wsgi.url_scheme': 'https', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '4333', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'https://example.com:4333') def test_application_url(self): inst = self._blankOne('/%C3%AB') inst.script_name = text_(b'/\xc3\xab', 'utf-8') app_url = inst.application_url self.assertEqual(app_url.__class__, str) self.assertEqual(app_url, 'http://localhost/%C3%AB') def test_path_url(self): inst = self._blankOne('/%C3%AB') inst.script_name = text_(b'/\xc3\xab', 'utf-8') app_url = inst.path_url self.assertEqual(app_url.__class__, str) self.assertEqual(app_url, 'http://localhost/%C3%AB/%C3%AB') def test_path(self): inst = self._blankOne('/%C3%AB') inst.script_name = text_(b'/\xc3\xab', 'utf-8') app_url = inst.path self.assertEqual(app_url.__class__, str) self.assertEqual(app_url, '/%C3%AB/%C3%AB') def test_path_qs_no_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) self.assertEqual(req.path_qs, '/script/path/info') def test_path_qs_w_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.path_qs, '/script/path/info?foo=bar&baz=bam') def test_url_no_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) self.assertEqual(req.url, 'http://example.com/script/path/info') def test_url_w_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.url, 'http://example.com/script/path/info?foo=bar&baz=bam') def test_relative_url_to_app_true_wo_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.relative_url('other/page', True), 'http://example.com/script/other/page') def test_relative_url_to_app_true_w_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.relative_url('/other/page', True), 'http://example.com/other/page') def test_relative_url_to_app_false_other_w_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.relative_url('/other/page', False), 'http://example.com/other/page') def test_relative_url_to_app_false_other_wo_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.relative_url('other/page', False), 'http://example.com/script/path/other/page') def test_path_info_pop_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '', } req = self._makeOne(environ) popped = req.path_info_pop() self.assertEqual(popped, None) self.assertEqual(environ['SCRIPT_NAME'], '/script') def test_path_info_pop_just_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/', } req = self._makeOne(environ) popped = req.path_info_pop() self.assertEqual(popped, '') self.assertEqual(environ['SCRIPT_NAME'], '/script/') self.assertEqual(environ['PATH_INFO'], '') def test_path_info_pop_non_empty_no_pattern(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop() self.assertEqual(popped, 'path') self.assertEqual(environ['SCRIPT_NAME'], '/script/path') self.assertEqual(environ['PATH_INFO'], '/info') def test_path_info_pop_non_empty_w_pattern_miss(self): import re PATTERN = re.compile('miss') environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop(PATTERN) self.assertEqual(popped, None) self.assertEqual(environ['SCRIPT_NAME'], '/script') self.assertEqual(environ['PATH_INFO'], '/path/info') def test_path_info_pop_non_empty_w_pattern_hit(self): import re PATTERN = re.compile('path') environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop(PATTERN) self.assertEqual(popped, 'path') self.assertEqual(environ['SCRIPT_NAME'], '/script/path') self.assertEqual(environ['PATH_INFO'], '/info') def test_path_info_pop_skips_empty_elements(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '//path/info', } req = self._makeOne(environ) popped = req.path_info_pop() self.assertEqual(popped, 'path') self.assertEqual(environ['SCRIPT_NAME'], '/script//path') self.assertEqual(environ['PATH_INFO'], '/info') def test_path_info_peek_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '', } req = self._makeOne(environ) peeked = req.path_info_peek() self.assertEqual(peeked, None) self.assertEqual(environ['SCRIPT_NAME'], '/script') self.assertEqual(environ['PATH_INFO'], '') def test_path_info_peek_just_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/', } req = self._makeOne(environ) peeked = req.path_info_peek() self.assertEqual(peeked, '') self.assertEqual(environ['SCRIPT_NAME'], '/script') self.assertEqual(environ['PATH_INFO'], '/') def test_path_info_peek_non_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path', } req = self._makeOne(environ) peeked = req.path_info_peek() self.assertEqual(peeked, 'path') self.assertEqual(environ['SCRIPT_NAME'], '/script') self.assertEqual(environ['PATH_INFO'], '/path') def test_is_xhr_no_header(self): req = self._makeOne({}) self.assertTrue(not req.is_xhr) def test_is_xhr_header_miss(self): environ = {'HTTP_X_REQUESTED_WITH': 'notAnXMLHTTPRequest'} req = self._makeOne(environ) self.assertTrue(not req.is_xhr) def test_is_xhr_header_hit(self): environ = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} req = self._makeOne(environ) self.assertTrue(req.is_xhr) # host def test_host_getter_w_HTTP_HOST(self): environ = {'HTTP_HOST': 'example.com:8888'} req = self._makeOne(environ) self.assertEqual(req.host, 'example.com:8888') def test_host_getter_wo_HTTP_HOST(self): environ = {'SERVER_NAME': 'example.com', 'SERVER_PORT': '8888'} req = self._makeOne(environ) self.assertEqual(req.host, 'example.com:8888') def test_host_setter(self): environ = {} req = self._makeOne(environ) req.host = 'example.com:8888' self.assertEqual(environ['HTTP_HOST'], 'example.com:8888') def test_host_deleter_hit(self): environ = {'HTTP_HOST': 'example.com:8888'} req = self._makeOne(environ) del req.host self.assertTrue('HTTP_HOST' not in environ) def test_host_deleter_miss(self): environ = {} req = self._makeOne(environ) del req.host # doesn't raise def test_domain_nocolon(self): environ = {'HTTP_HOST':'example.com'} req = self._makeOne(environ) self.assertEqual(req.domain, 'example.com') def test_domain_withcolon(self): environ = {'HTTP_HOST':'example.com:8888'} req = self._makeOne(environ) self.assertEqual(req.domain, 'example.com') def test_encget_raises_without_default(self): inst = self._makeOne({}) self.assertRaises(KeyError, inst.encget, 'a') def test_encget_doesnt_raises_with_default(self): inst = self._makeOne({}) self.assertEqual(inst.encget('a', None), None) def test_encget_with_encattr(self): if PY3: val = b'\xc3\xab'.decode('latin-1') else: val = b'\xc3\xab' inst = self._makeOne({'a':val}) self.assertEqual(inst.encget('a', encattr='url_encoding'), text_(b'\xc3\xab', 'utf-8')) def test_encget_with_encattr_latin_1(self): if PY3: val = b'\xc3\xab'.decode('latin-1') else: val = b'\xc3\xab' inst = self._makeOne({'a':val}) inst.my_encoding = 'latin-1' self.assertEqual(inst.encget('a', encattr='my_encoding'), text_(b'\xc3\xab', 'latin-1')) def test_encget_no_encattr(self): if PY3: val = b'\xc3\xab'.decode('latin-1') else: val = b'\xc3\xab' inst = self._makeOne({'a':val}) self.assertEqual(inst.encget('a'), val) def test_relative_url(self): inst = self._blankOne('/%C3%AB/c') result = inst.relative_url('a') self.assertEqual(result.__class__, str) self.assertEqual(result, 'http://localhost/%C3%AB/a') def test_header_getter(self): if PY3: val = b'abc'.decode('latin-1') else: val = b'abc' inst = self._makeOne({'HTTP_FLUB':val}) result = inst.headers['Flub'] self.assertEqual(result.__class__, str) self.assertEqual(result, 'abc') def test_json_body(self): inst = self._makeOne({}) inst.body = b'{"a":"1"}' self.assertEqual(inst.json_body, {'a':'1'}) inst.json_body = {'a': '2'} self.assertEqual(inst.body, b'{"a":"2"}') def test_host_get(self): inst = self._makeOne({'HTTP_HOST':'example.com'}) result = inst.host self.assertEqual(result.__class__, str) self.assertEqual(result, 'example.com') def test_host_get_w_no_http_host(self): inst = self._makeOne({'SERVER_NAME':'example.com', 'SERVER_PORT':'80'}) result = inst.host self.assertEqual(result.__class__, str) self.assertEqual(result, 'example.com:80') class TestLegacyRequest(unittest.TestCase): # tests of methods of a bytesrequest which deal with http environment vars def _getTargetClass(self): from webob.request import LegacyRequest return LegacyRequest def _makeOne(self, *arg, **kw): cls = self._getTargetClass() return cls(*arg, **kw) def _blankOne(self, *arg, **kw): cls = self._getTargetClass() return cls.blank(*arg, **kw) def test_method(self): environ = {'REQUEST_METHOD': 'OPTIONS', } req = self._makeOne(environ) self.assertEqual(req.method, 'OPTIONS') def test_http_version(self): environ = {'SERVER_PROTOCOL': '1.1', } req = self._makeOne(environ) self.assertEqual(req.http_version, '1.1') def test_script_name(self): environ = {'SCRIPT_NAME': '/script', } req = self._makeOne(environ) self.assertEqual(req.script_name, '/script') def test_path_info(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) self.assertEqual(req.path_info, '/path/info') def test_content_length_getter(self): environ = {'CONTENT_LENGTH': '1234', } req = self._makeOne(environ) self.assertEqual(req.content_length, 1234) def test_content_length_setter_w_str(self): environ = {'CONTENT_LENGTH': '1234', } req = self._makeOne(environ) req.content_length = '3456' self.assertEqual(req.content_length, 3456) def test_remote_user(self): environ = {'REMOTE_USER': 'phred', } req = self._makeOne(environ) self.assertEqual(req.remote_user, 'phred') def test_remote_addr(self): environ = {'REMOTE_ADDR': '1.2.3.4', } req = self._makeOne(environ) self.assertEqual(req.remote_addr, '1.2.3.4') def test_query_string(self): environ = {'QUERY_STRING': 'foo=bar&baz=bam', } req = self._makeOne(environ) self.assertEqual(req.query_string, 'foo=bar&baz=bam') def test_server_name(self): environ = {'SERVER_NAME': 'somehost.tld', } req = self._makeOne(environ) self.assertEqual(req.server_name, 'somehost.tld') def test_server_port_getter(self): environ = {'SERVER_PORT': '6666', } req = self._makeOne(environ) self.assertEqual(req.server_port, 6666) def test_server_port_setter_with_string(self): environ = {'SERVER_PORT': '6666', } req = self._makeOne(environ) req.server_port = '6667' self.assertEqual(req.server_port, 6667) def test_uscript_name(self): environ = {'SCRIPT_NAME': '/script', } req = self._makeOne(environ) self.assertTrue(isinstance(req.uscript_name, text_type)) result = req.uscript_name self.assertEqual(result.__class__, text_type) self.assertEqual(result, '/script') def test_upath_info(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) result = req.upath_info self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, '/path/info') def test_upath_info_set_unicode(self): environ = {'PATH_INFO': '/path/info', } req = self._makeOne(environ) req.upath_info = text_('/another') result = req.upath_info self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, '/another') def test_content_type_getter_no_parameters(self): environ = {'CONTENT_TYPE': 'application/xml+foobar', } req = self._makeOne(environ) self.assertEqual(req.content_type, 'application/xml+foobar') def test_content_type_getter_w_parameters(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) self.assertEqual(req.content_type, 'application/xml+foobar') def test_content_type_setter_w_None(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) req.content_type = None self.assertEqual(req.content_type, '') self.assertTrue('CONTENT_TYPE' not in environ) def test_content_type_setter_existing_paramter_no_new_paramter(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) req.content_type = 'text/xml' self.assertEqual(req.content_type, 'text/xml') self.assertEqual(environ['CONTENT_TYPE'], 'text/xml;charset="utf8"') def test_content_type_deleter_clears_environ_value(self): environ = {'CONTENT_TYPE': 'application/xml+foobar;charset="utf8"', } req = self._makeOne(environ) del req.content_type self.assertEqual(req.content_type, '') self.assertTrue('CONTENT_TYPE' not in environ) def test_content_type_deleter_no_environ_value(self): environ = {} req = self._makeOne(environ) del req.content_type self.assertEqual(req.content_type, '') self.assertTrue('CONTENT_TYPE' not in environ) def test_headers_getter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) headers = req.headers self.assertEqual(headers, {'Content-Type':CONTENT_TYPE, 'Content-Length': '123'}) def test_headers_setter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) req.headers = {'Qux': 'Spam'} self.assertEqual(req.headers, {'Qux': 'Spam'}) self.assertEqual(environ['HTTP_QUX'], native_('Spam')) self.assertEqual(environ, {'HTTP_QUX': 'Spam'}) def test_no_headers_deleter(self): CONTENT_TYPE = 'application/xml+foobar;charset="utf8"' environ = {'CONTENT_TYPE': CONTENT_TYPE, 'CONTENT_LENGTH': '123', } req = self._makeOne(environ) def _test(): del req.headers self.assertRaises(AttributeError, _test) def test_client_addr_xff_singleval(self): environ = { 'HTTP_X_FORWARDED_FOR': '192.168.1.1', } req = self._makeOne(environ) self.assertEqual(req.client_addr, '192.168.1.1') def test_client_addr_xff_multival(self): environ = { 'HTTP_X_FORWARDED_FOR': '192.168.1.1, 192.168.1.2', } req = self._makeOne(environ) self.assertEqual(req.client_addr, '192.168.1.1') def test_client_addr_prefers_xff(self): environ = {'REMOTE_ADDR': '192.168.1.2', 'HTTP_X_FORWARDED_FOR': '192.168.1.1', } req = self._makeOne(environ) self.assertEqual(req.client_addr, '192.168.1.1') def test_client_addr_no_xff(self): environ = {'REMOTE_ADDR': '192.168.1.2', } req = self._makeOne(environ) self.assertEqual(req.client_addr, '192.168.1.2') def test_client_addr_no_xff_no_remote_addr(self): environ = {} req = self._makeOne(environ) self.assertEqual(req.client_addr, None) def test_host_port_w_http_host_and_no_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) self.assertEqual(req.host_port, '80') def test_host_port_w_http_host_and_standard_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:80', } req = self._makeOne(environ) self.assertEqual(req.host_port, '80') def test_host_port_w_http_host_and_oddball_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) self.assertEqual(req.host_port, '8888') def test_host_port_w_http_host_https_and_no_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) self.assertEqual(req.host_port, '443') def test_host_port_w_http_host_https_and_standard_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:443', } req = self._makeOne(environ) self.assertEqual(req.host_port, '443') def test_host_port_w_http_host_https_and_oddball_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) self.assertEqual(req.host_port, '8888') def test_host_port_wo_http_host(self): environ = {'wsgi.url_scheme': 'https', 'SERVER_PORT': '4333', } req = self._makeOne(environ) self.assertEqual(req.host_port, '4333') def test_host_url_w_http_host_and_no_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'http://example.com') def test_host_url_w_http_host_and_standard_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:80', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'http://example.com') def test_host_url_w_http_host_and_oddball_port(self): environ = {'wsgi.url_scheme': 'http', 'HTTP_HOST': 'example.com:8888', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'http://example.com:8888') def test_host_url_w_http_host_https_and_no_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'https://example.com') def test_host_url_w_http_host_https_and_standard_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:443', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'https://example.com') def test_host_url_w_http_host_https_and_oddball_port(self): environ = {'wsgi.url_scheme': 'https', 'HTTP_HOST': 'example.com:4333', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'https://example.com:4333') def test_host_url_wo_http_host(self): environ = {'wsgi.url_scheme': 'https', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '4333', } req = self._makeOne(environ) self.assertEqual(req.host_url, 'https://example.com:4333') def test_application_url(self): inst = self._blankOne('/%C3%AB') inst.script_name = b'/\xc3\xab' app_url = inst.application_url if PY3: # pragma: no cover # this result is why you should not use legacyrequest under py 3 self.assertEqual(app_url, 'http://localhost/%C3%83%C2%AB') else: self.assertEqual(app_url, 'http://localhost/%C3%AB') def test_path_url(self): inst = self._blankOne('/%C3%AB') inst.script_name = b'/\xc3\xab' result = inst.path_url if PY3: # pragma: no cover # this result is why you should not use legacyrequest under py 3 self.assertEqual(result, 'http://localhost/%C3%83%C2%AB/%C3%83%C2%AB') else: self.assertEqual(result, 'http://localhost/%C3%AB/%C3%AB') def test_path(self): inst = self._blankOne('/%C3%AB') inst.script_name = b'/\xc3\xab' result = inst.path if PY3: # pragma: no cover # this result is why you should not use legacyrequest under py 3 self.assertEqual(result, '/%C3%83%C2%AB/%C3%83%C2%AB') else: self.assertEqual(result, '/%C3%AB/%C3%AB') def test_path_qs_no_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) self.assertEqual(req.path_qs, '/script/path/info') def test_path_qs_w_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.path_qs, '/script/path/info?foo=bar&baz=bam') def test_url_no_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) self.assertEqual(req.url, 'http://example.com/script/path/info') def test_url_w_qs(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.url, 'http://example.com/script/path/info?foo=bar&baz=bam') def test_relative_url_to_app_true_wo_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.relative_url('other/page', True), 'http://example.com/script/other/page') def test_relative_url_to_app_true_w_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.relative_url('/other/page', True), 'http://example.com/other/page') def test_relative_url_to_app_false_other_w_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.relative_url('/other/page', False), 'http://example.com/other/page') def test_relative_url_to_app_false_other_wo_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', 'QUERY_STRING': 'foo=bar&baz=bam' } req = self._makeOne(environ) self.assertEqual(req.relative_url('other/page', False), 'http://example.com/script/path/other/page') def test_path_info_pop_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '', } req = self._makeOne(environ) popped = req.path_info_pop() self.assertEqual(popped, None) self.assertEqual(environ['SCRIPT_NAME'], '/script') def test_path_info_pop_just_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/', } req = self._makeOne(environ) popped = req.path_info_pop() self.assertEqual(popped, '') self.assertEqual(environ['SCRIPT_NAME'], '/script/') self.assertEqual(environ['PATH_INFO'], '') def test_path_info_pop_non_empty_no_pattern(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop() self.assertEqual(popped, 'path') self.assertEqual(environ['SCRIPT_NAME'], '/script/path') self.assertEqual(environ['PATH_INFO'], '/info') def test_path_info_pop_non_empty_w_pattern_miss(self): import re PATTERN = re.compile('miss') environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop(PATTERN) self.assertEqual(popped, None) self.assertEqual(environ['SCRIPT_NAME'], '/script') self.assertEqual(environ['PATH_INFO'], '/path/info') def test_path_info_pop_non_empty_w_pattern_hit(self): import re PATTERN = re.compile('path') environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path/info', } req = self._makeOne(environ) popped = req.path_info_pop(PATTERN) self.assertEqual(popped, 'path') self.assertEqual(environ['SCRIPT_NAME'], '/script/path') self.assertEqual(environ['PATH_INFO'], '/info') def test_path_info_pop_skips_empty_elements(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '//path/info', } req = self._makeOne(environ) popped = req.path_info_pop() self.assertEqual(popped, 'path') self.assertEqual(environ['SCRIPT_NAME'], '/script//path') self.assertEqual(environ['PATH_INFO'], '/info') def test_path_info_peek_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '', } req = self._makeOne(environ) peeked = req.path_info_peek() self.assertEqual(peeked, None) self.assertEqual(environ['SCRIPT_NAME'], '/script') self.assertEqual(environ['PATH_INFO'], '') def test_path_info_peek_just_leading_slash(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/', } req = self._makeOne(environ) peeked = req.path_info_peek() self.assertEqual(peeked, '') self.assertEqual(environ['SCRIPT_NAME'], '/script') self.assertEqual(environ['PATH_INFO'], '/') def test_path_info_peek_non_empty(self): environ = {'wsgi.url_scheme': 'http', 'SERVER_NAME': 'example.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/script', 'PATH_INFO': '/path', } req = self._makeOne(environ) peeked = req.path_info_peek() self.assertEqual(peeked, 'path') self.assertEqual(environ['SCRIPT_NAME'], '/script') self.assertEqual(environ['PATH_INFO'], '/path') def test_is_xhr_no_header(self): req = self._makeOne({}) self.assertTrue(not req.is_xhr) def test_is_xhr_header_miss(self): environ = {'HTTP_X_REQUESTED_WITH': 'notAnXMLHTTPRequest'} req = self._makeOne(environ) self.assertTrue(not req.is_xhr) def test_is_xhr_header_hit(self): environ = {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'} req = self._makeOne(environ) self.assertTrue(req.is_xhr) # host def test_host_getter_w_HTTP_HOST(self): environ = {'HTTP_HOST': 'example.com:8888'} req = self._makeOne(environ) self.assertEqual(req.host, 'example.com:8888') def test_host_getter_wo_HTTP_HOST(self): environ = {'SERVER_NAME': 'example.com', 'SERVER_PORT': '8888'} req = self._makeOne(environ) self.assertEqual(req.host, 'example.com:8888') def test_host_setter(self): environ = {} req = self._makeOne(environ) req.host = 'example.com:8888' self.assertEqual(environ['HTTP_HOST'], 'example.com:8888') def test_host_deleter_hit(self): environ = {'HTTP_HOST': 'example.com:8888'} req = self._makeOne(environ) del req.host self.assertTrue('HTTP_HOST' not in environ) def test_host_deleter_miss(self): environ = {} req = self._makeOne(environ) del req.host # doesn't raise def test_encget_raises_without_default(self): inst = self._makeOne({}) self.assertRaises(KeyError, inst.encget, 'a') def test_encget_doesnt_raises_with_default(self): inst = self._makeOne({}) self.assertEqual(inst.encget('a', None), None) def test_encget_with_encattr(self): if PY3: val = b'\xc3\xab'.decode('latin-1') else: val = b'\xc3\xab' inst = self._makeOne({'a':val}) self.assertEqual(inst.encget('a', encattr='url_encoding'), native_(b'\xc3\xab', 'latin-1')) def test_encget_no_encattr(self): if PY3: val = b'\xc3\xab'.decode('latin-1') else: val = b'\xc3\xab' inst = self._makeOne({'a':val}) self.assertEqual(inst.encget('a'), native_(b'\xc3\xab', 'latin-1')) def test_relative_url(self): inst = self._blankOne('/%C3%AB/c') result = inst.relative_url('a') if PY3: # pragma: no cover # this result is why you should not use legacyrequest under py 3 self.assertEqual(result, 'http://localhost/%C3%83%C2%AB/a') else: self.assertEqual(result, 'http://localhost/%C3%AB/a') def test_header_getter(self): if PY3: val = b'abc'.decode('latin-1') else: val = b'abc' inst = self._makeOne({'HTTP_FLUB':val}) result = inst.headers['Flub'] self.assertEqual(result, 'abc') def test_json_body(self): inst = self._makeOne({}) inst.body = b'{"a":"1"}' self.assertEqual(inst.json_body, {'a':'1'}) def test_host_get_w_http_host(self): inst = self._makeOne({'HTTP_HOST':'example.com'}) result = inst.host self.assertEqual(result, 'example.com') def test_host_get_w_no_http_host(self): inst = self._makeOne({'SERVER_NAME':'example.com', 'SERVER_PORT':'80'}) result = inst.host self.assertEqual(result, 'example.com:80') class TestRequestConstructorWarnings(unittest.TestCase): def _getTargetClass(self): from webob.request import Request return Request def _makeOne(self, *arg, **kw): cls = self._getTargetClass() return cls(*arg, **kw) def test_ctor_w_unicode_errors(self): with warnings.catch_warnings(record=True) as w: # still emit if warning was printed previously warnings.simplefilter('always') self._makeOne({}, unicode_errors=True) self.assertEqual(len(w), 1) def test_ctor_w_decode_param_names(self): with warnings.catch_warnings(record=True) as w: # still emit if warning was printed previously warnings.simplefilter('always') self._makeOne({}, decode_param_names=True) self.assertEqual(len(w), 1) class TestRequestWithAdhocAttr(unittest.TestCase): def _blankOne(self, *arg, **kw): from webob.request import Request return Request.blank(*arg, **kw) def test_adhoc_attrs_set(self): req = self._blankOne('/') req.foo = 1 self.assertEqual(req.environ['webob.adhoc_attrs'], {'foo': 1}) def test_adhoc_attrs_set_nonadhoc(self): req = self._blankOne('/', environ={'webob.adhoc_attrs':{}}) req.request_body_tempfile_limit = 1 self.assertEqual(req.environ['webob.adhoc_attrs'], {}) def test_adhoc_attrs_get(self): req = self._blankOne('/', environ={'webob.adhoc_attrs': {'foo': 1}}) self.assertEqual(req.foo, 1) def test_adhoc_attrs_get_missing(self): req = self._blankOne('/') self.assertRaises(AttributeError, getattr, req, 'some_attr') def test_adhoc_attrs_del(self): req = self._blankOne('/', environ={'webob.adhoc_attrs': {'foo': 1}}) del req.foo self.assertEqual(req.environ['webob.adhoc_attrs'], {}) def test_adhoc_attrs_del_missing(self): req = self._blankOne('/') self.assertRaises(AttributeError, delattr, req, 'some_attr') class TestRequest_functional(unittest.TestCase): # functional tests of request def _getTargetClass(self): from webob.request import Request return Request def _makeOne(self, *arg, **kw): cls = self._getTargetClass() return cls(*arg, **kw) def _blankOne(self, *arg, **kw): cls = self._getTargetClass() return cls.blank(*arg, **kw) def test_gets(self): request = self._blankOne('/') status, headerlist, app_iter = request.call_application(simpleapp) self.assertEqual(status, '200 OK') res = b''.join(app_iter) self.assertTrue(b'Hello' in res) self.assertTrue(b"MultiDict([])" in res) self.assertTrue(b"post is " in res) def test_gets_with_query_string(self): request = self._blankOne('/?name=george') status, headerlist, app_iter = request.call_application(simpleapp) res = b''.join(app_iter) self.assertTrue(b"MultiDict" in res) self.assertTrue(b"'name'" in res) self.assertTrue(b"'george'" in res) self.assertTrue(b"Val is " in res) def test_language_parsing1(self): request = self._blankOne('/') status, headerlist, app_iter = request.call_application(simpleapp) res = b''.join(app_iter) self.assertTrue(b"The languages are: []" in res) def test_language_parsing2(self): request = self._blankOne( '/', headers={'Accept-Language': 'da, en-gb;q=0.8'}) status, headerlist, app_iter = request.call_application(simpleapp) res = b''.join(app_iter) self.assertTrue(b"languages are: ['da', 'en-gb']" in res) def test_language_parsing3(self): request = self._blankOne( '/', headers={'Accept-Language': 'en-gb;q=0.8, da'}) status, headerlist, app_iter = request.call_application(simpleapp) res = b''.join(app_iter) self.assertTrue(b"languages are: ['da', 'en-gb']" in res) def test_mime_parsing1(self): request = self._blankOne( '/', headers={'Accept':'text/html'}) status, headerlist, app_iter = request.call_application(simpleapp) res = b''.join(app_iter) self.assertTrue(b"accepttypes is: text/html" in res) def test_mime_parsing2(self): request = self._blankOne( '/', headers={'Accept':'application/xml'}) status, headerlist, app_iter = request.call_application(simpleapp) res = b''.join(app_iter) self.assertTrue(b"accepttypes is: application/xml" in res) def test_mime_parsing3(self): request = self._blankOne( '/', headers={'Accept':'application/xml,*/*'}) status, headerlist, app_iter = request.call_application(simpleapp) res = b''.join(app_iter) self.assertTrue(b"accepttypes is: application/xml" in res) def test_accept_best_match(self): accept = self._blankOne('/').accept self.assertTrue(not accept) self.assertTrue(not self._blankOne('/', headers={'Accept': ''}).accept) req = self._blankOne('/', headers={'Accept':'text/plain'}) self.assertTrue(req.accept) self.assertRaises(ValueError, req.accept.best_match, ['*/*']) req = self._blankOne('/', accept=['*/*','text/*']) self.assertEqual( req.accept.best_match(['application/x-foo', 'text/plain']), 'text/plain') self.assertEqual( req.accept.best_match(['text/plain', 'application/x-foo']), 'text/plain') req = self._blankOne('/', accept=['text/plain', 'message/*']) self.assertEqual( req.accept.best_match(['message/x-foo', 'text/plain']), 'text/plain') self.assertEqual( req.accept.best_match(['text/plain', 'message/x-foo']), 'text/plain') def test_from_mimeparse(self): # http://mimeparse.googlecode.com/svn/trunk/mimeparse.py supported = ['application/xbel+xml', 'application/xml'] tests = [('application/xbel+xml', 'application/xbel+xml'), ('application/xbel+xml; q=1', 'application/xbel+xml'), ('application/xml; q=1', 'application/xml'), ('application/*; q=1', 'application/xbel+xml'), ('*/*', 'application/xbel+xml')] for accept, get in tests: req = self._blankOne('/', headers={'Accept':accept}) self.assertEqual(req.accept.best_match(supported), get) supported = ['application/xbel+xml', 'text/xml'] tests = [('text/*;q=0.5,*/*; q=0.1', 'text/xml'), ('text/html,application/atom+xml; q=0.9', None)] for accept, get in tests: req = self._blankOne('/', headers={'Accept':accept}) self.assertEqual(req.accept.best_match(supported), get) supported = ['application/json', 'text/html'] tests = [ ('application/json, text/javascript, */*', 'application/json'), ('application/json, text/html;q=0.9', 'application/json'), ] for accept, get in tests: req = self._blankOne('/', headers={'Accept':accept}) self.assertEqual(req.accept.best_match(supported), get) offered = ['image/png', 'application/xml'] tests = [ ('image/png', 'image/png'), ('image/*', 'image/png'), ('image/*, application/xml', 'application/xml'), ] for accept, get in tests: req = self._blankOne('/', accept=accept) self.assertEqual(req.accept.best_match(offered), get) def test_headers(self): headers = { 'If-Modified-Since': 'Sat, 29 Oct 1994 19:43:31 GMT', 'Cookie': 'var1=value1', 'User-Agent': 'Mozilla 4.0 (compatible; MSIE)', 'If-None-Match': '"etag001", "etag002"', 'X-Requested-With': 'XMLHttpRequest', } request = self._blankOne('/?foo=bar&baz', headers=headers) status, headerlist, app_iter = request.call_application(simpleapp) res = b''.join(app_iter) for thing in ( 'if_modified_since: ' + 'datetime.datetime(1994, 10, 29, 19, 43, 31, tzinfo=UTC)', "user_agent: 'Mozilla", 'is_xhr: True', "cookies is ', ): self.assertTrue(bytes_(thing) in res) def test_bad_cookie(self): req = self._blankOne('/') req.headers['Cookie'] = '070-it-:>')) def test_from_garbage_file(self): # If we pass a file with garbage to from_file method it should # raise an error plus missing bits in from_file method io = BytesIO(b'hello world') cls = self._getTargetClass() self.assertRaises(ValueError, cls.from_file, io) val_file = BytesIO( b"GET /webob/ HTTP/1.1\n" b"Host: pythonpaste.org\n" b"User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.13)" b"Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13\n" b"Accept: " b"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;" b"q=0.8\n" b"Accept-Language: en-us,en;q=0.5\n" b"Accept-Encoding: gzip,deflate\n" b"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\n" # duplicate on purpose b"Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7\n" b"Keep-Alive: 115\n" b"Connection: keep-alive\n" ) req = cls.from_file(val_file) self.assertTrue(isinstance(req, cls)) self.assertTrue(not repr(req).endswith('(invalid WSGI environ)>')) val_file = BytesIO( b"GET /webob/ HTTP/1.1\n" b"Host pythonpaste.org\n" ) self.assertRaises(ValueError, cls.from_file, val_file) def test_from_bytes(self): # A valid request without a Content-Length header should still read # the full body. # Also test parity between as_string and from_bytes / from_file. import cgi cls = self._getTargetClass() req = cls.from_bytes(_test_req) self.assertTrue(isinstance(req, cls)) self.assertTrue(not repr(req).endswith('(invalid WSGI environ)>')) self.assertTrue('\n' not in req.http_version or '\r' in req.http_version) self.assertTrue(',' not in req.host) self.assertTrue(req.content_length is not None) self.assertEqual(req.content_length, 337) self.assertTrue(b'foo' in req.body) bar_contents = b"these are the contents of the file 'bar.txt'\r\n" self.assertTrue(bar_contents in req.body) self.assertEqual(req.params['foo'], 'foo') bar = req.params['bar'] self.assertTrue(isinstance(bar, cgi.FieldStorage)) self.assertEqual(bar.type, 'application/octet-stream') bar.file.seek(0) self.assertEqual(bar.file.read(), bar_contents) # out should equal contents, except for the Content-Length header, # so insert that. _test_req_copy = _test_req.replace( b'Content-Type', b'Content-Length: 337\r\nContent-Type' ) self.assertEqual(req.as_bytes(), _test_req_copy) req2 = cls.from_bytes(_test_req2) self.assertTrue('host' not in req2.headers) self.assertEqual(req2.as_bytes(), _test_req2.rstrip()) self.assertRaises(ValueError, cls.from_bytes, _test_req2 + b'xx') def test_from_text(self): import cgi cls = self._getTargetClass() req = cls.from_text(text_(_test_req, 'utf-8')) self.assertTrue(isinstance(req, cls)) self.assertTrue(not repr(req).endswith('(invalid WSGI environ)>')) self.assertTrue('\n' not in req.http_version or '\r' in req.http_version) self.assertTrue(',' not in req.host) self.assertTrue(req.content_length is not None) self.assertEqual(req.content_length, 337) self.assertTrue(b'foo' in req.body) bar_contents = b"these are the contents of the file 'bar.txt'\r\n" self.assertTrue(bar_contents in req.body) self.assertEqual(req.params['foo'], 'foo') bar = req.params['bar'] self.assertTrue(isinstance(bar, cgi.FieldStorage)) self.assertEqual(bar.type, 'application/octet-stream') bar.file.seek(0) self.assertEqual(bar.file.read(), bar_contents) # out should equal contents, except for the Content-Length header, # so insert that. _test_req_copy = _test_req.replace( b'Content-Type', b'Content-Length: 337\r\nContent-Type' ) self.assertEqual(req.as_bytes(), _test_req_copy) req2 = cls.from_bytes(_test_req2) self.assertTrue('host' not in req2.headers) self.assertEqual(req2.as_bytes(), _test_req2.rstrip()) self.assertRaises(ValueError, cls.from_bytes, _test_req2 + b'xx') def test_blank(self): # BaseRequest.blank class method self.assertRaises(ValueError, self._blankOne, 'www.example.com/foo?hello=world', None, 'www.example.com/foo?hello=world') self.assertRaises(ValueError, self._blankOne, 'gopher.example.com/foo?hello=world', None, 'gopher://gopher.example.com') req = self._blankOne('www.example.com/foo?hello=world', None, 'http://www.example.com') self.assertEqual(req.environ.get('HTTP_HOST', None), 'www.example.com:80') self.assertEqual(req.environ.get('PATH_INFO', None), 'www.example.com/foo') self.assertEqual(req.environ.get('QUERY_STRING', None), 'hello=world') self.assertEqual(req.environ.get('REQUEST_METHOD', None), 'GET') req = self._blankOne('www.example.com/secure?hello=world', None, 'https://www.example.com/secure') self.assertEqual(req.environ.get('HTTP_HOST', None), 'www.example.com:443') self.assertEqual(req.environ.get('PATH_INFO', None), 'www.example.com/secure') self.assertEqual(req.environ.get('QUERY_STRING', None), 'hello=world') self.assertEqual(req.environ.get('REQUEST_METHOD', None), 'GET') self.assertEqual(req.environ.get('SCRIPT_NAME', None), '/secure') self.assertEqual(req.environ.get('SERVER_NAME', None), 'www.example.com') self.assertEqual(req.environ.get('SERVER_PORT', None), '443') def test_post_does_not_reparse(self): # test that there's no repetitive parsing is happening on every # req.POST access req = self._blankOne('/', content_type='multipart/form-data; boundary=boundary', POST=_cgi_escaping_body ) f0 = req.body_file_raw post1 = req.POST f1 = req.body_file_raw self.assertTrue(f1 is not f0) post2 = req.POST f2 = req.body_file_raw self.assertTrue(post1 is post2) self.assertTrue(f1 is f2) def test_middleware_body(self): def app(env, sr): sr('200 OK', []) return [env['wsgi.input'].read()] def mw(env, sr): req = self._makeOne(env) data = req.body_file.read() resp = req.get_response(app) resp.headers['x-data'] = data return resp(env, sr) req = self._blankOne('/', method='PUT', body=b'abc') resp = req.get_response(mw) self.assertEqual(resp.body, b'abc') self.assertEqual(resp.headers['x-data'], b'abc') def test_body_file_noseek(self): req = self._blankOne('/', method='PUT', body=b'abc') lst = [req.body_file.read(1) for i in range(3)] self.assertEqual(lst, [b'a', b'b', b'c']) def test_cgi_escaping_fix(self): req = self._blankOne('/', content_type='multipart/form-data; boundary=boundary', POST=_cgi_escaping_body ) self.assertEqual(list(req.POST.keys()), ['%20%22"']) req.body_file.read() self.assertEqual(list(req.POST.keys()), ['%20%22"']) def test_content_type_none(self): r = self._blankOne('/', content_type='text/html') self.assertEqual(r.content_type, 'text/html') r.content_type = None def test_body_file_seekable(self): r = self._blankOne('/', method='POST') r.body_file = BytesIO(b'body') self.assertEqual(r.body_file_seekable.read(), b'body') def test_request_init(self): # port from doctest (docs/reference.txt) req = self._blankOne('/article?id=1') self.assertEqual(req.environ['HTTP_HOST'], 'localhost:80') self.assertEqual(req.environ['PATH_INFO'], '/article') self.assertEqual(req.environ['QUERY_STRING'], 'id=1') self.assertEqual(req.environ['REQUEST_METHOD'], 'GET') self.assertEqual(req.environ['SCRIPT_NAME'], '') self.assertEqual(req.environ['SERVER_NAME'], 'localhost') self.assertEqual(req.environ['SERVER_PORT'], '80') self.assertEqual(req.environ['SERVER_PROTOCOL'], 'HTTP/1.0') self.assertTrue(hasattr(req.environ['wsgi.errors'], 'write') and hasattr(req.environ['wsgi.errors'], 'flush')) self.assertTrue(hasattr(req.environ['wsgi.input'], 'next') or hasattr(req.environ['wsgi.input'], '__next__')) self.assertEqual(req.environ['wsgi.multiprocess'], False) self.assertEqual(req.environ['wsgi.multithread'], False) self.assertEqual(req.environ['wsgi.run_once'], False) self.assertEqual(req.environ['wsgi.url_scheme'], 'http') self.assertEqual(req.environ['wsgi.version'], (1, 0)) # Test body self.assertTrue(hasattr(req.body_file, 'read')) self.assertEqual(req.body, b'') req.method = 'PUT' req.body = b'test' self.assertTrue(hasattr(req.body_file, 'read')) self.assertEqual(req.body, b'test') # Test method & URL self.assertEqual(req.method, 'PUT') self.assertEqual(req.scheme, 'http') self.assertEqual(req.script_name, '') # The base of the URL req.script_name = '/blog' # make it more interesting self.assertEqual(req.path_info, '/article') # Content-Type of the request body self.assertEqual(req.content_type, '') # The auth'ed user (there is none set) self.assertTrue(req.remote_user is None) self.assertTrue(req.remote_addr is None) self.assertEqual(req.host, 'localhost:80') self.assertEqual(req.host_url, 'http://localhost') self.assertEqual(req.application_url, 'http://localhost/blog') self.assertEqual(req.path_url, 'http://localhost/blog/article') self.assertEqual(req.url, 'http://localhost/blog/article?id=1') self.assertEqual(req.path, '/blog/article') self.assertEqual(req.path_qs, '/blog/article?id=1') self.assertEqual(req.query_string, 'id=1') self.assertEqual(req.relative_url('archive'), 'http://localhost/blog/archive') # Doesn't change request self.assertEqual(req.path_info_peek(), 'article') # Does change request! self.assertEqual(req.path_info_pop(), 'article') self.assertEqual(req.script_name, '/blog/article') self.assertEqual(req.path_info, '') # Headers req.headers['Content-Type'] = 'application/x-www-urlencoded' self.assertEqual(sorted(req.headers.items()), [('Content-Length', '4'), ('Content-Type', 'application/x-www-urlencoded'), ('Host', 'localhost:80')]) self.assertEqual(req.environ['CONTENT_TYPE'], 'application/x-www-urlencoded') def test_request_query_and_POST_vars(self): # port from doctest (docs/reference.txt) # Query & POST variables from webob.multidict import MultiDict from webob.multidict import NestedMultiDict from webob.multidict import NoVars from webob.multidict import GetDict req = self._blankOne('/test?check=a&check=b&name=Bob') GET = GetDict([('check', 'a'), ('check', 'b'), ('name', 'Bob')], {}) self.assertEqual(req.GET, GET) self.assertEqual(req.GET['check'], 'b') self.assertEqual(req.GET.getall('check'), ['a', 'b']) self.assertEqual(list(req.GET.items()), [('check', 'a'), ('check', 'b'), ('name', 'Bob')]) self.assertTrue(isinstance(req.POST, NoVars)) # NoVars can be read like a dict, but not written self.assertEqual(list(req.POST.items()), []) req.method = 'POST' req.body = b'name=Joe&email=joe@example.com' self.assertEqual(req.POST, MultiDict([('name', 'Joe'), ('email', 'joe@example.com')])) self.assertEqual(req.POST['name'], 'Joe') self.assertTrue(isinstance(req.params, NestedMultiDict)) self.assertEqual(list(req.params.items()), [('check', 'a'), ('check', 'b'), ('name', 'Bob'), ('name', 'Joe'), ('email', 'joe@example.com')]) self.assertEqual(req.params['name'], 'Bob') self.assertEqual(req.params.getall('name'), ['Bob', 'Joe']) def test_request_put(self): from datetime import datetime from webob import Response from webob import UTC from webob.acceptparse import MIMEAccept from webob.byterange import Range from webob.etag import ETagMatcher from webob.multidict import MultiDict from webob.multidict import GetDict req = self._blankOne('/test?check=a&check=b&name=Bob') req.method = 'PUT' req.body = b'var1=value1&var2=value2&rep=1&rep=2' req.environ['CONTENT_LENGTH'] = str(len(req.body)) req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' GET = GetDict([('check', 'a'), ('check', 'b'), ('name', 'Bob')], {}) self.assertEqual(req.GET, GET) self.assertEqual(req.POST, MultiDict( [('var1', 'value1'), ('var2', 'value2'), ('rep', '1'), ('rep', '2')])) self.assertEqual( list(req.GET.items()), [('check', 'a'), ('check', 'b'), ('name', 'Bob')]) # Unicode req.charset = 'utf8' self.assertEqual(list(req.GET.items()), [('check', 'a'), ('check', 'b'), ('name', 'Bob')]) # Cookies req.headers['Cookie'] = 'test=value' self.assertTrue(isinstance(req.cookies, collections.MutableMapping)) self.assertEqual(list(req.cookies.items()), [('test', 'value')]) req.charset = None self.assertEqual(req.cookies, {'test': 'value'}) # Accept-* headers self.assertTrue('text/html' in req.accept) req.accept = 'text/html;q=0.5, application/xhtml+xml;q=1' self.assertTrue(isinstance(req.accept, MIMEAccept)) self.assertTrue('text/html' in req.accept) self.assertRaises(DeprecationWarning, req.accept.first_match, ['text/html']) self.assertEqual(req.accept.best_match(['text/html', 'application/xhtml+xml']), 'application/xhtml+xml') req.accept_language = 'es, pt-BR' self.assertEqual(req.accept_language.best_match(['es']), 'es') # Conditional Requests server_token = 'opaque-token' # shouldn't return 304 self.assertTrue(not server_token in req.if_none_match) req.if_none_match = server_token self.assertTrue(isinstance(req.if_none_match, ETagMatcher)) # You *should* return 304 self.assertTrue(server_token in req.if_none_match) # if_none_match should use weak matching weak_token = 'W/"%s"' % server_token req.if_none_match = weak_token assert req.headers['if-none-match'] == weak_token self.assertTrue(server_token in req.if_none_match) req.if_modified_since = datetime(2006, 1, 1, 12, 0, tzinfo=UTC) self.assertEqual(req.headers['If-Modified-Since'], 'Sun, 01 Jan 2006 12:00:00 GMT') server_modified = datetime(2005, 1, 1, 12, 0, tzinfo=UTC) self.assertTrue(req.if_modified_since) self.assertTrue(req.if_modified_since >= server_modified) self.assertTrue(not req.if_range) self.assertTrue(Response(etag='some-etag', last_modified=datetime(2005, 1, 1, 12, 0)) in req.if_range) req.if_range = 'opaque-etag' self.assertTrue(Response(etag='other-etag') not in req.if_range) self.assertTrue(Response(etag='opaque-etag') in req.if_range) res = Response(etag='opaque-etag') self.assertTrue(res in req.if_range) req.range = 'bytes=0-100' self.assertTrue(isinstance(req.range, Range)) self.assertEqual(tuple(req.range), (0, 101)) cr = req.range.content_range(length=1000) self.assertEqual(tuple(cr), (0, 101, 1000)) self.assertTrue(server_token in req.if_match) # No If-Match means everything is ok req.if_match = server_token self.assertTrue(server_token in req.if_match) # Still OK req.if_match = 'other-token' # Not OK, should return 412 Precondition Failed: self.assertTrue(not server_token in req.if_match) def test_request_patch(self): from webob.multidict import MultiDict from webob.multidict import GetDict req = self._blankOne('/test?check=a&check=b&name=Bob') req.method = 'PATCH' req.body = b'var1=value1&var2=value2&rep=1&rep=2' req.environ['CONTENT_LENGTH'] = str(len(req.body)) req.environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' GET = GetDict([('check', 'a'), ('check', 'b'), ('name', 'Bob')], {}) self.assertEqual(req.GET, GET) self.assertEqual(req.POST, MultiDict( [('var1', 'value1'), ('var2', 'value2'), ('rep', '1'), ('rep', '2')])) def test_call_WSGI_app(self): req = self._blankOne('/') def wsgi_app(environ, start_response): start_response('200 OK', [('Content-type', 'text/plain')]) return [b'Hi!'] self.assertEqual(req.call_application(wsgi_app), ('200 OK', [('Content-type', 'text/plain')], [b'Hi!'])) res = req.get_response(wsgi_app) from webob.response import Response self.assertTrue(isinstance(res, Response)) self.assertEqual(res.status, '200 OK') from webob.headers import ResponseHeaders self.assertTrue(isinstance(res.headers, ResponseHeaders)) self.assertEqual(list(res.headers.items()), [('Content-type', 'text/plain')]) self.assertEqual(res.body, b'Hi!') def test_get_response_catch_exc_info_true(self): req = self._blankOne('/') def wsgi_app(environ, start_response): start_response('200 OK', [('Content-type', 'text/plain')]) return [b'Hi!'] res = req.get_response(wsgi_app, catch_exc_info=True) from webob.response import Response self.assertTrue(isinstance(res, Response)) self.assertEqual(res.status, '200 OK') from webob.headers import ResponseHeaders self.assertTrue(isinstance(res.headers, ResponseHeaders)) self.assertEqual(list(res.headers.items()), [('Content-type', 'text/plain')]) self.assertEqual(res.body, b'Hi!') def equal_req(self, req, inp): cls = self._getTargetClass() req2 = cls.from_file(inp) self.assertEqual(req.url, req2.url) headers1 = dict(req.headers) headers2 = dict(req2.headers) self.assertEqual(int(headers1.get('Content-Length', '0')), int(headers2.get('Content-Length', '0'))) if 'Content-Length' in headers1: del headers1['Content-Length'] if 'Content-Length' in headers2: del headers2['Content-Length'] self.assertEqual(headers1, headers2) req_body = req.body req2_body = req2.body self.assertEqual(req_body, req2_body) class FakeCGIBodyTests(unittest.TestCase): def test_encode_multipart_value_type_options(self): from cgi import FieldStorage from webob.request import BaseRequest, FakeCGIBody from webob.multidict import MultiDict multipart_type = 'multipart/form-data; boundary=foobar' from io import BytesIO body = ( b'--foobar\r\n' b'Content-Disposition: form-data; name="bananas"; ' b'filename="bananas.txt"\r\n' b'Content-type: text/plain; charset="utf-7"\r\n' b'\r\n' b"these are the contents of the file 'bananas.txt'\r\n" b'\r\n' b'--foobar--') multipart_body = BytesIO(body) environ = BaseRequest.blank('/').environ environ.update(CONTENT_TYPE=multipart_type) environ.update(REQUEST_METHOD='POST') environ.update(CONTENT_LENGTH=len(body)) fs = FieldStorage(multipart_body, environ=environ) vars = MultiDict.from_fieldstorage(fs) self.assertEqual(vars['bananas'].__class__, FieldStorage) fake_body = FakeCGIBody(vars, multipart_type) self.assertEqual(fake_body.read(), body) def test_encode_multipart_no_boundary(self): from webob.request import FakeCGIBody self.assertRaises(ValueError, FakeCGIBody, {}, 'multipart/form-data') def test_repr(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'multipart/form-data; boundary=foobar') body.read(1) import re self.assertEqual( re.sub(r'\b0x[0-9a-f]+\b', '', repr(body)), " viewing {'bananas': 'ba...nas'}>", ) def test_fileno(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'multipart/form-data; boundary=foobar') self.assertEqual(body.fileno(), None) def test_iter(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'multipart/form-data; boundary=foobar') self.assertEqual(list(body), [ b'--foobar\r\n', b'Content-Disposition: form-data; name="bananas"\r\n', b'\r\n', b'bananas\r\n', b'--foobar--', ]) def test_readline(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'multipart/form-data; boundary=foobar') self.assertEqual(body.readline(), b'--foobar\r\n') self.assertEqual( body.readline(), b'Content-Disposition: form-data; name="bananas"\r\n') self.assertEqual(body.readline(), b'\r\n') self.assertEqual(body.readline(), b'bananas\r\n') self.assertEqual(body.readline(), b'--foobar--') # subsequent calls to readline will return '' def test_read_bad_content_type(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'application/jibberjabber') self.assertRaises(AssertionError, body.read) def test_read_urlencoded(self): from webob.request import FakeCGIBody body = FakeCGIBody({'bananas': 'bananas'}, 'application/x-www-form-urlencoded') self.assertEqual(body.read(), b'bananas=bananas') class Test_cgi_FieldStorage__repr__patch(unittest.TestCase): def _callFUT(self, fake): from webob.request import _cgi_FieldStorage__repr__patch return _cgi_FieldStorage__repr__patch(fake) def test_with_file(self): class Fake(object): name = 'name' file = 'file' filename = 'filename' value = 'value' fake = Fake() result = self._callFUT(fake) self.assertEqual(result, "FieldStorage('name', 'filename')") def test_without_file(self): class Fake(object): name = 'name' file = None filename = 'filename' value = 'value' fake = Fake() result = self._callFUT(fake) self.assertEqual(result, "FieldStorage('name', 'filename', 'value')") class TestLimitedLengthFile(unittest.TestCase): def _makeOne(self, file, maxlen): from webob.request import LimitedLengthFile return LimitedLengthFile(file, maxlen) def test_fileno(self): class DummyFile(object): def fileno(self): return 1 dummyfile = DummyFile() inst = self._makeOne(dummyfile, 0) self.assertEqual(inst.fileno(), 1) class Test_environ_from_url(unittest.TestCase): def _callFUT(self, *arg, **kw): from webob.request import environ_from_url return environ_from_url(*arg, **kw) def test_environ_from_url(self): # Generating an environ just from an url plus testing environ_add_POST self.assertRaises(TypeError, self._callFUT, 'http://www.example.com/foo?bar=baz#qux') self.assertRaises(TypeError, self._callFUT, 'gopher://gopher.example.com') req = self._callFUT('http://www.example.com/foo?bar=baz') self.assertEqual(req.get('HTTP_HOST', None), 'www.example.com:80') self.assertEqual(req.get('PATH_INFO', None), '/foo') self.assertEqual(req.get('QUERY_STRING', None), 'bar=baz') self.assertEqual(req.get('REQUEST_METHOD', None), 'GET') self.assertEqual(req.get('SCRIPT_NAME', None), '') self.assertEqual(req.get('SERVER_NAME', None), 'www.example.com') self.assertEqual(req.get('SERVER_PORT', None), '80') req = self._callFUT('https://www.example.com/foo?bar=baz') self.assertEqual(req.get('HTTP_HOST', None), 'www.example.com:443') self.assertEqual(req.get('PATH_INFO', None), '/foo') self.assertEqual(req.get('QUERY_STRING', None), 'bar=baz') self.assertEqual(req.get('REQUEST_METHOD', None), 'GET') self.assertEqual(req.get('SCRIPT_NAME', None), '') self.assertEqual(req.get('SERVER_NAME', None), 'www.example.com') self.assertEqual(req.get('SERVER_PORT', None), '443') from webob.request import environ_add_POST environ_add_POST(req, None) self.assertTrue('CONTENT_TYPE' not in req) self.assertTrue('CONTENT_LENGTH' not in req) environ_add_POST(req, {'hello':'world'}) self.assertTrue(req.get('HTTP_HOST', None), 'www.example.com:443') self.assertEqual(req.get('PATH_INFO', None), '/foo') self.assertEqual(req.get('QUERY_STRING', None), 'bar=baz') self.assertEqual(req.get('REQUEST_METHOD', None), 'POST') self.assertEqual(req.get('SCRIPT_NAME', None), '') self.assertEqual(req.get('SERVER_NAME', None), 'www.example.com') self.assertEqual(req.get('SERVER_PORT', None), '443') self.assertEqual(req.get('CONTENT_LENGTH', None),'11') self.assertEqual(req.get('CONTENT_TYPE', None), 'application/x-www-form-urlencoded') self.assertEqual(req['wsgi.input'].read(), b'hello=world') def test_environ_from_url_highorder_path_info(self): from webob.request import Request env = self._callFUT('/%E6%B5%81') self.assertEqual(env['PATH_INFO'], '/\xe6\xb5\x81') request = Request(env) expected = text_(b'/\xe6\xb5\x81', 'utf-8') # u'/\u6d41' self.assertEqual(request.path_info, expected) self.assertEqual(request.upath_info, expected) def test_fileupload_mime_type_detection(self): from webob.request import Request # sometimes on win the detected mime type for .jpg will be # image/pjpeg for ex. so use a non-standard extesion to avoid that import mimetypes mimetypes.add_type('application/x-foo', '.foo') request = Request.blank("/", POST=dict(file1=("foo.foo", "xxx"), file2=("bar.mp3", "xxx"))) self.assertTrue("audio/mpeg" in request.body.decode('ascii'), str(request)) self.assertTrue('application/x-foo' in request.body.decode('ascii'), str(request)) class TestRequestMultipart(unittest.TestCase): def test_multipart_with_charset(self): from webob.request import Request req = Request.from_string(_test_req_multipart_charset) self.assertEqual(req.POST['title'].encode('utf8'), text_('こんにちは', 'utf-8').encode('utf8')) def simpleapp(environ, start_response): from webob.request import Request status = '200 OK' response_headers = [('Content-type','text/plain')] start_response(status, response_headers) request = Request(environ) request.remote_user = 'bob' return [ bytes_(x) for x in [ 'Hello world!\n', 'The get is %r' % request.GET, ' and Val is %s\n' % repr(request.GET.get('name')), 'The languages are: %s\n' % list(request.accept_language), 'The accepttypes is: %s\n' % request.accept.best_match(['application/xml', 'text/html']), 'post is %r\n' % request.POST, 'params is %r\n' % request.params, 'cookies is %r\n' % request.cookies, 'body: %r\n' % request.body, 'method: %s\n' % request.method, 'remote_user: %r\n' % request.environ['REMOTE_USER'], 'host_url: %r; application_url: %r; path_url: %r; url: %r\n' % (request.host_url, request.application_url, request.path_url, request.url), 'urlvars: %r\n' % request.urlvars, 'urlargs: %r\n' % (request.urlargs, ), 'is_xhr: %r\n' % request.is_xhr, 'if_modified_since: %r\n' % request.if_modified_since, 'user_agent: %r\n' % request.user_agent, 'if_none_match: %r\n' % request.if_none_match, ]] _cgi_escaping_body = '''--boundary Content-Disposition: form-data; name="%20%22"" --boundary--''' def _norm_req(s): return b'\r\n'.join(s.strip().replace(b'\r', b'').split(b'\n')) _test_req = b""" POST /webob/ HTTP/1.0 Accept: */* Cache-Control: max-age=0 Content-Type: multipart/form-data; boundary=----------------------------deb95b63e42a Host: pythonpaste.org User-Agent: UserAgent/1.0 (identifier-version) library/7.0 otherlibrary/0.8 ------------------------------deb95b63e42a Content-Disposition: form-data; name="foo" foo ------------------------------deb95b63e42a Content-Disposition: form-data; name="bar"; filename="bar.txt" Content-type: application/octet-stream these are the contents of the file 'bar.txt' ------------------------------deb95b63e42a-- """ _test_req2 = b""" POST / HTTP/1.0 Content-Length: 0 """ _test_req_multipart_charset = b""" POST /upload/ HTTP/1.1 Host: foo.com User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.2.13) Gecko/20101206 Ubuntu/10.04 (lucid) Firefox/3.6.13 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: en-US,en;q=0.8,ja;q=0.6 Accept-Encoding: gzip,deflate Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7 Content-Type: multipart/form-data; boundary=000e0ce0b196b4ee6804c6c8af94 Content-Length: 926 --000e0ce0b196b4ee6804c6c8af94 Content-Type: text/plain; charset=ISO-2022-JP Content-Disposition: form-data; name=title Content-Transfer-Encoding: 7bit \x1b$B$3$s$K$A$O\x1b(B --000e0ce0b196b4ee6804c6c8af94 Content-Type: text/plain; charset=ISO-8859-1 Content-Disposition: form-data; name=submit Submit --000e0ce0b196b4ee6804c6c8af94 Content-Type: message/external-body; charset=ISO-8859-1; blob-key=AMIfv94TgpPBtKTL3a0U9Qh1QCX7OWSsmdkIoD2ws45kP9zQAGTOfGNz4U18j7CVXzODk85WtiL5gZUFklTGY3y4G0Jz3KTPtJBOFDvQHQew7YUymRIpgUXgENS_fSEmInAIQdpSc2E78MRBVEZY392uhph3r-In96t8Z58WIRc-Yikx1bnarWo Content-Disposition: form-data; name=file; filename="photo.jpg" Content-Type: image/jpeg Content-Length: 38491 X-AppEngine-Upload-Creation: 2012-08-08 15:32:29.035959 Content-MD5: ZjRmNGRhYmNhZTkyNzcyOWQ5ZGUwNDgzOWFkNDAxN2Y= Content-Disposition: form-data; name=file; filename="photo.jpg" --000e0ce0b196b4ee6804c6c8af94--""" _test_req = _norm_req(_test_req) _test_req2 = _norm_req(_test_req2) + b'\r\n' _test_req_multipart_charset = _norm_req(_test_req_multipart_charset) class UnseekableInput(object): def __init__(self, data): self.data = data self.pos = 0 def read(self, size=-1): if size == -1: t = self.data[self.pos:] self.pos = len(self.data) return t else: assert(self.pos + size <= len(self.data)) t = self.data[self.pos:self.pos+size] self.pos += size return t class UnseekableInputWithSeek(UnseekableInput): def seek(self, pos, rel=0): raise IOError("Invalid seek!") WebOb-1.3.1/tests/test_etag_nose.py0000644000175000017500000000460111645621753020040 0ustar chrismchrism00000000000000from webob.etag import IfRange, ETagMatcher from webob import Response from nose.tools import eq_, assert_raises def test_if_range_None(): ir = IfRange.parse(None) eq_(str(ir), '') assert not ir assert Response() in ir assert Response(etag='foo') in ir assert Response(etag='foo GMT') in ir def test_if_range_match_date(): date = 'Fri, 09 Nov 2001 01:08:47 GMT' ir = IfRange.parse(date) eq_(str(ir), date) assert Response() not in ir assert Response(etag='etag') not in ir assert Response(etag=date) not in ir assert Response(last_modified='Fri, 09 Nov 2001 01:00:00 GMT') in ir assert Response(last_modified='Fri, 10 Nov 2001 01:00:00 GMT') not in ir def test_if_range_match_etag(): ir = IfRange.parse('ETAG') eq_(str(ir), '"ETAG"') assert Response() not in ir assert Response(etag='other') not in ir assert Response(etag='ETAG') in ir assert Response(etag='W/"ETAG"') not in ir def test_if_range_match_etag_weak(): ir = IfRange.parse('W/"ETAG"') eq_(str(ir), '') assert Response(etag='ETAG') not in ir assert Response(etag='W/"ETAG"') not in ir def test_if_range_repr(): eq_(repr(IfRange.parse(None)), 'IfRange()') eq_(str(IfRange.parse(None)), '') def test_resp_etag(): def t(tag, res, raw, strong): eq_(Response(etag=tag).etag, res) eq_(Response(etag=tag).headers.get('etag'), raw) eq_(Response(etag=tag).etag_strong, strong) t('foo', 'foo', '"foo"', 'foo') t('"foo"', 'foo', '"foo"', 'foo') t('a"b', 'a"b', '"a\\"b"', 'a"b') t('W/"foo"', 'foo', 'W/"foo"', None) t('W/"a\\"b"', 'a"b', 'W/"a\\"b"', None) t(('foo', True), 'foo', '"foo"', 'foo') t(('foo', False), 'foo', 'W/"foo"', None) t(('"foo"', True), '"foo"', r'"\"foo\""', '"foo"') t(('W/"foo"', True), 'W/"foo"', r'"W/\"foo\""', 'W/"foo"') t(('W/"foo"', False), 'W/"foo"', r'W/"W/\"foo\""', None) def test_matcher(): matcher = ETagMatcher(['ETAGS']) matcher = ETagMatcher(['ETAGS']) eq_(matcher.etags, ['ETAGS']) assert_raises(DeprecationWarning, matcher.weak_match, "etag") assert "ETAGS" in matcher assert "WEAK" not in matcher assert "BEER" not in matcher assert None not in matcher eq_(repr(matcher), '') eq_(str(matcher), '"ETAGS"') matcher2 = ETagMatcher(("ETAG1","ETAG2")) eq_(repr(matcher2), '') WebOb-1.3.1/tests/test_datetime_utils.py0000644000175000017500000001150411645621753021110 0ustar chrismchrism00000000000000# -*- coding: utf-8 -*- import datetime import calendar from email.utils import formatdate from webob import datetime_utils from nose.tools import ok_, eq_, assert_raises def test_UTC(): """Test missing function in _UTC""" x = datetime_utils.UTC ok_(x.tzname(datetime.datetime.now())=='UTC') eq_(x.dst(datetime.datetime.now()), datetime.timedelta(0)) eq_(x.utcoffset(datetime.datetime.now()), datetime.timedelta(0)) eq_(repr(x), 'UTC') def test_parse_date(): """Testing datetime_utils.parse_date. We need to verify the following scenarios: * a nil submitted value * a submitted value that cannot be parse into a date * a valid RFC2822 date with and without timezone """ ret = datetime_utils.parse_date(None) ok_(ret is None, "We passed a None value " "to parse_date. We should get None but instead we got %s" %\ ret) ret = datetime_utils.parse_date('Hi There') ok_(ret is None, "We passed an invalid value " "to parse_date. We should get None but instead we got %s" %\ ret) ret = datetime_utils.parse_date(1) ok_(ret is None, "We passed an invalid value " "to parse_date. We should get None but instead we got %s" %\ ret) ret = datetime_utils.parse_date('á') ok_(ret is None, "We passed an invalid value " "to parse_date. We should get None but instead we got %s" %\ ret) ret = datetime_utils.parse_date('Mon, 20 Nov 1995 19:12:08 -0500') eq_(ret, datetime.datetime( 1995, 11, 21, 0, 12, 8, tzinfo=datetime_utils.UTC)) ret = datetime_utils.parse_date('Mon, 20 Nov 1995 19:12:08') eq_(ret, datetime.datetime(1995, 11, 20, 19, 12, 8, tzinfo=datetime_utils.UTC)) ret = datetime_utils.parse_date(Uncooperative()) eq_(ret, None) class Uncooperative(object): def __str__(self): raise NotImplementedError def test_serialize_date(): """Testing datetime_utils.serialize_date We need to verify the following scenarios: * on py3, passing an binary date, return the same date but str * on py2, passing an unicode date, return the same date but str * passing a timedelta, return now plus the delta * passing an invalid object, should raise ValueError """ from webob.compat import text_ ret = datetime_utils.serialize_date('Mon, 20 Nov 1995 19:12:08 GMT') assert isinstance(ret, str) eq_(ret, 'Mon, 20 Nov 1995 19:12:08 GMT') ret = datetime_utils.serialize_date(text_('Mon, 20 Nov 1995 19:12:08 GMT')) assert isinstance(ret, str) eq_(ret, 'Mon, 20 Nov 1995 19:12:08 GMT') dt = formatdate( calendar.timegm( (datetime.datetime.now()+datetime.timedelta(1)).timetuple()), usegmt=True) eq_(dt, datetime_utils.serialize_date(datetime.timedelta(1))) assert_raises(ValueError, datetime_utils.serialize_date, None) def test_parse_date_delta(): """Testing datetime_utils.parse_date_delta We need to verify the following scenarios: * passing a nil value, should return nil * passing a value that fails the conversion to int, should call parse_date """ ok_(datetime_utils.parse_date_delta(None) is None, 'Passing none value, ' 'should return None') ret = datetime_utils.parse_date_delta('Mon, 20 Nov 1995 19:12:08 -0500') eq_(ret, datetime.datetime( 1995, 11, 21, 0, 12, 8, tzinfo=datetime_utils.UTC)) WHEN = datetime.datetime(2011, 3, 16, 10, 10, 37, tzinfo=datetime_utils.UTC) #with _NowRestorer(WHEN): Dammit, only Python 2.5 w/ __future__ nr = _NowRestorer(WHEN) nr.__enter__() try: ret = datetime_utils.parse_date_delta(1) eq_(ret, WHEN + datetime.timedelta(0, 1)) finally: nr.__exit__(None, None, None) def test_serialize_date_delta(): """Testing datetime_utils.serialize_date_delta We need to verify the following scenarios: * if we pass something that's not an int or float, it should delegate the task to serialize_date """ eq_(datetime_utils.serialize_date_delta(1), '1') eq_(datetime_utils.serialize_date_delta(1.5), '1') ret = datetime_utils.serialize_date_delta('Mon, 20 Nov 1995 19:12:08 GMT') assert type(ret) is (str) eq_(ret, 'Mon, 20 Nov 1995 19:12:08 GMT') def test_timedelta_to_seconds(): val = datetime.timedelta(86400) result = datetime_utils.timedelta_to_seconds(val) eq_(result, 7464960000) class _NowRestorer(object): def __init__(self, new_now): self._new_now = new_now self._old_now = None def __enter__(self): import webob.datetime_utils self._old_now = webob.datetime_utils._now webob.datetime_utils._now = lambda: self._new_now def __exit__(self, exc_type, exc_value, traceback): import webob.datetime_utils webob.datetime_utils._now = self._old_now WebOb-1.3.1/tests/test_cookies.py0000664000175000017500000005112412252633462017527 0ustar chrismchrism00000000000000# -*- coding: utf-8 -*- from datetime import timedelta from webob import cookies from webob.compat import text_ from nose.tools import eq_ import unittest from webob.compat import native_ from webob.compat import PY3 def test_cookie_empty(): c = cookies.Cookie() # empty cookie eq_(repr(c), '') def test_cookie_one_value(): c = cookies.Cookie('dismiss-top=6') vals = list(c.values()) eq_(len(vals), 1) eq_(vals[0].name, b'dismiss-top') eq_(vals[0].value, b'6') def test_cookie_one_value_with_trailing_semi(): c = cookies.Cookie('dismiss-top=6;') vals = list(c.values()) eq_(len(vals), 1) eq_(vals[0].name, b'dismiss-top') eq_(vals[0].value, b'6') c = cookies.Cookie('dismiss-top=6;') def test_cookie_escaped_unquoted(): eq_(list(cookies.parse_cookie('x=\\040')), [(b'x', b' ')]) def test_cookie_complex(): c = cookies.Cookie('dismiss-top=6; CP=null*, '\ 'PHPSESSID=0a539d42abc001cdc762809248d4beed, a="42,"') d = lambda v: v.decode('ascii') c_dict = dict((d(k),d(v.value)) for k,v in c.items()) eq_(c_dict, {'a': '42,', 'CP': 'null*', 'PHPSESSID': '0a539d42abc001cdc762809248d4beed', 'dismiss-top': '6' }) def test_cookie_complex_serialize(): c = cookies.Cookie('dismiss-top=6; CP=null*, '\ 'PHPSESSID=0a539d42abc001cdc762809248d4beed, a="42,"') eq_(c.serialize(), 'CP=null*; PHPSESSID=0a539d42abc001cdc762809248d4beed; a="42\\054"; ' 'dismiss-top=6') def test_cookie_load_multiple(): c = cookies.Cookie('a=1; Secure=true') vals = list(c.values()) eq_(len(vals), 1) eq_(c[b'a'][b'secure'], b'true') def test_cookie_secure(): c = cookies.Cookie() c[text_('foo')] = b'bar' c[b'foo'].secure = True eq_(c.serialize(), 'foo=bar; secure') def test_cookie_httponly(): c = cookies.Cookie() c['foo'] = b'bar' c[b'foo'].httponly = True eq_(c.serialize(), 'foo=bar; HttpOnly') def test_cookie_reserved_keys(): c = cookies.Cookie('dismiss-top=6; CP=null*; $version=42; a=42') assert '$version' not in c c = cookies.Cookie('$reserved=42; a=$42') eq_(list(c.keys()), [b'a']) def test_serialize_cookie_date(): """ Testing webob.cookies.serialize_cookie_date. Missing scenarios: * input value is an str, should be returned verbatim * input value is an int, should be converted to timedelta and we should continue the rest of the process """ eq_(cookies.serialize_cookie_date(b'Tue, 04-Jan-2011 13:43:50 GMT'), b'Tue, 04-Jan-2011 13:43:50 GMT') eq_(cookies.serialize_cookie_date(text_('Tue, 04-Jan-2011 13:43:50 GMT')), b'Tue, 04-Jan-2011 13:43:50 GMT') eq_(cookies.serialize_cookie_date(None), None) cdate_delta = cookies.serialize_cookie_date(timedelta(seconds=10)) cdate_int = cookies.serialize_cookie_date(10) eq_(cdate_delta, cdate_int) def test_ch_unquote(): eq_(cookies._unquote(b'"hello world'), b'"hello world') eq_(cookies._unquote(b'hello world'), b'hello world') eq_(cookies._unquote(b'"hello world"'), b'hello world') eq_(cookies._value_quote(b'hello world'), b'"hello world"') # quotation mark escaped w/ backslash is unquoted correctly (support # pre webob 1.3 cookies) eq_(cookies._unquote(b'"\\""'), b'"') # we also are able to unquote the newer \\042 serialization of quotation # mark eq_(cookies._unquote(b'"\\042"'), b'"') # but when we generate a new cookie, quote using normal octal quoting # rules eq_(cookies._value_quote(b'"'), b'"\\042"') # backslash escaped w/ backslash is unquoted correctly (support # pre webob 1.3 cookies) eq_(cookies._unquote(b'"\\\\"'), b'\\') # we also are able to unquote the newer \\134 serialization of backslash eq_(cookies._unquote(b'"\\134"'), b'\\') # but when we generate a new cookie, quote using normal octal quoting # rules eq_(cookies._value_quote(b'\\'), b'"\\134"') # misc byte escaped as octal eq_(cookies._unquote(b'"\\377"'), b'\xff') eq_(cookies._value_quote(b'\xff'), b'"\\377"') # combination eq_(cookies._unquote(b'"a\\"\\377"'), b'a"\xff') eq_(cookies._value_quote(b'a"\xff'), b'"a\\042\\377"') def test_cookie_invalid_name(): c = cookies.Cookie() c['La Pe\xc3\xb1a'] = '1' eq_(len(c), 0) def test_morsel_serialize_with_expires(): morsel = cookies.Morsel(b'bleh', b'blah') morsel.expires = b'Tue, 04-Jan-2011 13:43:50 GMT' result = morsel.serialize() eq_(result, 'bleh=blah; expires=Tue, 04-Jan-2011 13:43:50 GMT') def test_serialize_max_age_timedelta(): import datetime val = datetime.timedelta(86400) result = cookies.serialize_max_age(val) eq_(result, b'7464960000') def test_serialize_max_age_int(): val = 86400 result = cookies.serialize_max_age(val) eq_(result, b'86400') def test_serialize_max_age_str(): val = '86400' result = cookies.serialize_max_age(val) eq_(result, b'86400') def test_escape_comma_semi_dquote(): c = cookies.Cookie() c['x'] = b'";,"' eq_(c.serialize(True), r'x="\042\073\054\042"') def test_parse_qmark_in_val(): v = r'x="\"\073\054\""; expires=Sun, 12-Jun-2011 23:16:01 GMT' c = cookies.Cookie(v) eq_(c[b'x'].value, b'";,"') eq_(c[b'x'].expires, b'Sun, 12-Jun-2011 23:16:01 GMT') def test_morsel_repr(): v = cookies.Morsel(b'a', b'b') result = repr(v) eq_(result, "") def test_strings_differ(): from webob.util import strings_differ eq_(strings_differ('test1', 'test'), True) class TestRequestCookies(unittest.TestCase): def _makeOne(self, environ): from webob.cookies import RequestCookies return RequestCookies(environ) def test_get_no_cache_key_in_environ_no_http_cookie_header(self): environ = {} inst = self._makeOne(environ) self.assertEqual(inst.get('a'), None) parsed = environ['webob._parsed_cookies'] self.assertEqual(parsed, ({}, '')) def test_get_no_cache_key_in_environ_has_http_cookie_header(self): header ='a=1; b=2' environ = {'HTTP_COOKIE':header} inst = self._makeOne(environ) self.assertEqual(inst.get('a'), '1') parsed = environ['webob._parsed_cookies'][0] self.assertEqual(parsed['a'], '1') self.assertEqual(parsed['b'], '2') self.assertEqual(environ['HTTP_COOKIE'], header) # no change def test_get_cache_key_in_environ_no_http_cookie_header(self): environ = {'webob._parsed_cookies':({}, '')} inst = self._makeOne(environ) self.assertEqual(inst.get('a'), None) parsed = environ['webob._parsed_cookies'] self.assertEqual(parsed, ({}, '')) def test_get_cache_key_in_environ_has_http_cookie_header(self): header ='a=1; b=2' environ = {'HTTP_COOKIE':header, 'webob._parsed_cookies':({}, '')} inst = self._makeOne(environ) self.assertEqual(inst.get('a'), '1') parsed = environ['webob._parsed_cookies'][0] self.assertEqual(parsed['a'], '1') self.assertEqual(parsed['b'], '2') self.assertEqual(environ['HTTP_COOKIE'], header) # no change def test_get_missing_with_default(self): environ = {} inst = self._makeOne(environ) self.assertEqual(inst.get('a', ''), '') def test___setitem__name_not_string_type(self): inst = self._makeOne({}) self.assertRaises(TypeError, inst.__setitem__, None, 1) def test___setitem__name_not_encodeable_to_ascii(self): name = native_(b'La Pe\xc3\xb1a', 'utf-8') inst = self._makeOne({}) self.assertRaises(TypeError, inst.__setitem__, name, 'abc') def test___setitem__name_not_rfc2109_valid(self): name = '$a' inst = self._makeOne({}) self.assertRaises(TypeError, inst.__setitem__, name, 'abc') def test___setitem__value_not_string_type(self): inst = self._makeOne({}) self.assertRaises(ValueError, inst.__setitem__, 'a', None) def test___setitem__value_not_utf_8_decodeable(self): value = text_(b'La Pe\xc3\xb1a', 'utf-8') value = value.encode('utf-16') inst = self._makeOne({}) self.assertRaises(ValueError, inst.__setitem__, 'a', value) def test__setitem__success_no_existing_headers(self): value = native_(b'La Pe\xc3\xb1a', 'utf-8') environ = {} inst = self._makeOne(environ) inst['a'] = value self.assertEqual(environ['HTTP_COOKIE'], 'a="La Pe\\303\\261a"') def test__setitem__success_append(self): value = native_(b'La Pe\xc3\xb1a', 'utf-8') environ = {'HTTP_COOKIE':'a=1; b=2'} inst = self._makeOne(environ) inst['c'] = value self.assertEqual( environ['HTTP_COOKIE'], 'a=1; b=2; c="La Pe\\303\\261a"') def test__setitem__success_replace(self): environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) inst['b'] = 'abc' self.assertEqual(environ['HTTP_COOKIE'], 'a=1; b=abc; c=3') inst['c'] = '4' self.assertEqual(environ['HTTP_COOKIE'], 'a=1; b=abc; c=4') def test__delitem__fail_no_http_cookie(self): environ = {} inst = self._makeOne(environ) self.assertRaises(KeyError, inst.__delitem__, 'a') self.assertEqual(environ, {}) def test__delitem__fail_with_http_cookie(self): environ = {'HTTP_COOKIE':''} inst = self._makeOne(environ) self.assertRaises(KeyError, inst.__delitem__, 'a') self.assertEqual(environ, {'HTTP_COOKIE':''}) def test__delitem__success(self): environ = {'HTTP_COOKIE':'a=1'} inst = self._makeOne(environ) del inst['a'] self.assertEqual(environ['HTTP_COOKIE'], '') self.assertEqual(inst._cache, {}) def test_keys(self): environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) self.assertEqual(sorted(list(inst.keys())), ['a', 'b', 'c']) def test_values(self): val = text_(b'La Pe\xc3\xb1a', 'utf-8') environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) self.assertEqual(sorted(list(inst.values())), ['1', '3', val]) def test_items(self): val = text_(b'La Pe\xc3\xb1a', 'utf-8') environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) self.assertEqual(sorted(list(inst.items())), [('a', '1'), ('b', val), ('c', '3')]) if not PY3: def test_iterkeys(self): environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) self.assertEqual(sorted(list(inst.iterkeys())), ['a', 'b', 'c']) def test_itervalues(self): val = text_(b'La Pe\xc3\xb1a', 'utf-8') environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) self.assertEqual(sorted(list(inst.itervalues())), ['1', '3', val]) def test_iteritems(self): val = text_(b'La Pe\xc3\xb1a', 'utf-8') environ = {'HTTP_COOKIE':'a=1; b="La Pe\\303\\261a"; c=3'} inst = self._makeOne(environ) self.assertEqual(sorted(list(inst.iteritems())), [('a', '1'), ('b', val), ('c', '3')]) def test___contains__(self): environ = {'HTTP_COOKIE':'a=1'} inst = self._makeOne(environ) self.assertTrue('a' in inst) self.assertFalse('b' in inst) def test___iter__(self): environ = {'HTTP_COOKIE':'a=1; b=2; c=3'} inst = self._makeOne(environ) self.assertEqual(sorted(list(iter(inst))), ['a', 'b', 'c']) def test___len__(self): environ = {'HTTP_COOKIE':'a=1; b=2; c=3'} inst = self._makeOne(environ) self.assertEqual(len(inst), 3) del inst['a'] self.assertEqual(len(inst), 2) def test_clear(self): environ = {'HTTP_COOKIE':'a=1; b=2; c=3'} inst = self._makeOne(environ) inst.clear() self.assertEqual(environ['HTTP_COOKIE'], '') self.assertEqual(inst.get('a'), None) def test___repr__(self): environ = {'HTTP_COOKIE':'a=1; b=2; c=3'} inst = self._makeOne(environ) r = repr(inst) self.assertTrue(r.startswith( '')) class CookieMakeCookie(unittest.TestCase): def makeOne(self, name, value, **kw): from webob.cookies import make_cookie return make_cookie(name, value, **kw) def test_make_cookie_max_age(self): cookie = self.makeOne('test_cookie', 'value', max_age=500) self.assertTrue('test_cookie=value' in cookie) self.assertTrue('Max-Age=500;' in cookie) self.assertTrue('expires' in cookie) def test_make_cookie_max_age_timedelta(self): from datetime import timedelta cookie = self.makeOne('test_cookie', 'value', max_age=timedelta(seconds=500)) self.assertTrue('test_cookie=value' in cookie) self.assertTrue('Max-Age=500;' in cookie) self.assertTrue('expires' in cookie) def test_make_cookie_comment(self): cookie = self.makeOne('test_cookie', 'value', comment='lolwhy') self.assertTrue('test_cookie=value' in cookie) self.assertTrue('Comment=lolwhy' in cookie) def test_make_cookie_path(self): cookie = self.makeOne('test_cookie', 'value', path='/foo/bar/baz') self.assertTrue('test_cookie=value' in cookie) self.assertTrue('Path=/foo/bar/baz' in cookie) class CommonCookieProfile(unittest.TestCase): def makeDummyRequest(self, **kw): class Dummy(object): def __init__(self, **kwargs): self.__dict__.update(**kwargs) d = Dummy(**kw) d.response = Dummy() d.response.headerlist = list() return d def makeOneRequest(self): request = self.makeDummyRequest(environ=dict()) request.environ['HTTP_HOST'] = 'www.example.net' request.cookies = dict() return request class CookieProfileTest(CommonCookieProfile): def makeOne(self, name='uns', **kw): if 'request' in kw: request = kw['request'] del kw['request'] else: request = self.makeOneRequest() from webob.cookies import CookieProfile return CookieProfile(name, **kw)(request) def test_cookie_creation(self): cookie = self.makeOne() from webob.cookies import CookieProfile self.assertTrue(isinstance(cookie, CookieProfile)) def test_cookie_name(self): cookie = self.makeOne() cookie_list = cookie.get_headers("test") for cookie in cookie_list: self.assertTrue(cookie[1].startswith('uns')) self.assertFalse('uns="";' in cookie[1]) def test_cookie_no_request(self): from webob.cookies import CookieProfile cookie = CookieProfile('uns') self.assertRaises(ValueError, cookie.get_value) def test_get_value_serializer_raises_value_error(self): class RaisingSerializer(object): def loads(self, val): raise ValueError('foo') cookie = self.makeOne(serializer=RaisingSerializer()) self.assertEqual(cookie.get_value(), None) class SignedCookieProfileTest(CommonCookieProfile): def makeOne(self, secret='seekrit', salt='salty', name='uns', **kw): if 'request' in kw: request = kw['request'] del kw['request'] else: request = self.makeOneRequest() from webob.cookies import SignedCookieProfile as CookieProfile return CookieProfile(secret, salt, name, **kw)(request) def test_cookie_name(self): cookie = self.makeOne() cookie_list = cookie.get_headers("test") for cookie in cookie_list: self.assertTrue(cookie[1].startswith('uns')) self.assertFalse('uns="";' in cookie[1]) def test_cookie_expire(self): cookie = self.makeOne() cookie_list = cookie.get_headers(None, max_age=0) for cookie in cookie_list: self.assertTrue('Max-Age=0;' in cookie[1]) def test_cookie_max_age(self): cookie = self.makeOne() cookie_list = cookie.get_headers("test", max_age=60) for cookie in cookie_list: self.assertTrue('Max-Age=60;' in cookie[1]) self.assertTrue('expires=' in cookie[1]) def test_cookie_raw(self): cookie = self.makeOne() cookie_list = cookie.get_headers("test") self.assertTrue(isinstance(cookie_list, list)) def test_set_cookie(self): request = self.makeOneRequest() cookie = self.makeOne(request=request) ret = cookie.set_cookies(request.response, "test") self.assertEqual(ret, request.response) def test_no_cookie(self): cookie = self.makeOne() ret = cookie.get_value() self.assertEqual(None, ret) def test_with_cookies(self): request = self.makeOneRequest() request.cookies['uns'] = ( 'FLIoEwZcKG6ITQSqbYcUNnPljwOcGNs25JRVCSoZcx_uX-OA1AhssA-CNeVKpWksQ' 'a0ktMhuQDdjzmDwgzbptiJ0ZXN0Ig' ) cookie = self.makeOne(request=request) ret = cookie.get_value() self.assertEqual(ret, "test") def test_with_bad_cookie_invalid_base64(self): request = self.makeOneRequest() request.cookies['uns'] = ( "gAJVBHRlc3RxAS4KjKfwGmCkliC4ba99rWUdpy_{}riHzK7MQFPsbSgYTgALHa" "SHrRkd3lyE8c4w5ruxAKOyj2h5oF69Ix7ERZv_") cookie = self.makeOne(request=request) val = cookie.get_value() self.assertEqual(val, None) def test_with_bad_cookie_invalid_signature(self): request = self.makeOneRequest() request.cookies['uns'] = ( "InRlc3QiFLIoEwZcKG6ITQSqbYcUNnPljwOcGNs25JRVCSoZcx/uX+OA1AhssA" "+CNeVKpWksQa0ktMhuQDdjzmDwgzbptg==") cookie = self.makeOne(secret='sekrit!', request=request) val = cookie.get_value() self.assertEqual(val, None) def test_with_domain(self): cookie = self.makeOne(domains=("testing.example.net",)) ret = cookie.get_headers("test") passed = False for cookie in ret: if 'Domain=testing.example.net' in cookie[1]: passed = True self.assertTrue(passed) self.assertEqual(len(ret), 1) def test_with_domains(self): cookie = self.makeOne( domains=("testing.example.net", "testing2.example.net") ) ret = cookie.get_headers("test") passed = 0 for cookie in ret: if 'Domain=testing.example.net' in cookie[1]: passed += 1 if 'Domain=testing2.example.net' in cookie[1]: passed += 1 self.assertEqual(passed, 2) self.assertEqual(len(ret), 2) def test_flag_secure(self): cookie = self.makeOne(secure=True) ret = cookie.get_headers("test") for cookie in ret: self.assertTrue('; secure' in cookie[1]) def test_flag_http_only(self): cookie = self.makeOne(httponly=True) ret = cookie.get_headers("test") for cookie in ret: self.assertTrue('; HttpOnly' in cookie[1]) def test_cookie_length(self): cookie = self.makeOne() longstring = 'a' * 4096 self.assertRaises(ValueError, cookie.get_headers, longstring) def test_very_long_key(self): longstring = 'a' * 1024 cookie = self.makeOne(secret=longstring) cookie.get_headers("test") def serialize(secret, salt, data): import hmac import base64 import json from hashlib import sha1 from webob.compat import bytes_ salted_secret = bytes_(salt or '') + bytes_(secret) cstruct = bytes_(json.dumps(data)) sig = hmac.new(salted_secret, cstruct, sha1).digest() return base64.urlsafe_b64encode(sig + cstruct).rstrip(b'=') class SignedSerializerTest(unittest.TestCase): def makeOne(self, secret, salt, hashalg='sha1', **kw): from webob.cookies import SignedSerializer return SignedSerializer(secret, salt, hashalg=hashalg, **kw) def test_serialize(self): ser = self.makeOne('seekrit', 'salty') self.assertEqual( ser.dumps('test'), serialize('seekrit', 'salty', 'test') ) def test_deserialize(self): ser = self.makeOne('seekrit', 'salty') self.assertEqual( ser.loads(serialize('seekrit', 'salty', 'test')), 'test' ) WebOb-1.3.1/tests/test_response.py0000664000175000017500000010027312127135226017725 0ustar chrismchrism00000000000000import zlib import io from nose.tools import eq_, ok_, assert_raises from webob.request import BaseRequest from webob.request import Request from webob.response import Response from webob.compat import text_ from webob.compat import bytes_ def simple_app(environ, start_response): start_response('200 OK', [ ('Content-Type', 'text/html; charset=utf8'), ]) return ['OK'] def test_response(): req = BaseRequest.blank('/') res = req.get_response(simple_app) assert res.status == '200 OK' assert res.status_code == 200 assert res.body == "OK" assert res.charset == 'utf8' assert res.content_type == 'text/html' res.status = 404 assert res.status == '404 Not Found' assert res.status_code == 404 res.body = b'Not OK' assert b''.join(res.app_iter) == b'Not OK' res.charset = 'iso8859-1' assert res.headers['content-type'] == 'text/html; charset=iso8859-1' res.content_type = 'text/xml' assert res.headers['content-type'] == 'text/xml; charset=iso8859-1' res.headers = {'content-type': 'text/html'} assert res.headers['content-type'] == 'text/html' assert res.headerlist == [('content-type', 'text/html')] res.set_cookie('x', 'y') assert res.headers['set-cookie'].strip(';') == 'x=y; Path=/' res.set_cookie(text_('x'), text_('y')) assert res.headers['set-cookie'].strip(';') == 'x=y; Path=/' res = Response('a body', '200 OK', content_type='text/html') res.encode_content() assert res.content_encoding == 'gzip' eq_(res.body, b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xffKTH\xcaO\xa9\x04\x00\xf6\x86GI\x06\x00\x00\x00') res.decode_content() assert res.content_encoding is None assert res.body == b'a body' res.set_cookie('x', text_(b'foo')) # test unicode value assert_raises(TypeError, Response, app_iter=iter(['a']), body="somebody") del req.environ assert_raises(TypeError, Response, charset=None, body=text_(b"unicode body")) assert_raises(TypeError, Response, wrong_key='dummy') def test_set_response_status_binary(): req = BaseRequest.blank('/') res = req.get_response(simple_app) res.status == b'200 OK' assert res.status_code == 200 assert res.status == '200 OK' def test_set_response_status_str_no_reason(): req = BaseRequest.blank('/') res = req.get_response(simple_app) res.status = '200' assert res.status_code == 200 assert res.status == '200 OK' def test_set_response_status_str_generic_reason(): req = BaseRequest.blank('/') res = req.get_response(simple_app) res.status = '299' assert res.status_code == 299 assert res.status == '299 Success' def test_set_response_status_code(): req = BaseRequest.blank('/') res = req.get_response(simple_app) res.status_code = 200 assert res.status_code == 200 assert res.status == '200 OK' def test_set_response_status_code_generic_reason(): req = BaseRequest.blank('/') res = req.get_response(simple_app) res.status_code = 299 assert res.status_code == 299 assert res.status == '299 Success' def test_content_type(): r = Response() # default ctype and charset eq_(r.content_type, 'text/html') eq_(r.charset, 'UTF-8') # setting to none, removes the header r.content_type = None eq_(r.content_type, None) eq_(r.charset, None) # can set missing ctype r.content_type = None eq_(r.content_type, None) def test_init_content_type_w_charset(): v = 'text/plain;charset=ISO-8859-1' eq_(Response(content_type=v).headers['content-type'], v) def test_cookies(): res = Response() # test unicode value res.set_cookie('x', text_(b'\N{BLACK SQUARE}', 'unicode_escape')) # utf8 encoded eq_(res.headers.getall('set-cookie'), ['x="\\342\\226\\240"; Path=/']) r2 = res.merge_cookies(simple_app) r2 = BaseRequest.blank('/').get_response(r2) eq_(r2.headerlist, [('Content-Type', 'text/html; charset=utf8'), ('Set-Cookie', 'x="\\342\\226\\240"; Path=/'), ] ) def test_http_only_cookie(): req = Request.blank('/') res = req.get_response(Response('blah')) res.set_cookie("foo", "foo", httponly=True) eq_(res.headers['set-cookie'], 'foo=foo; Path=/; HttpOnly') def test_headers(): r = Response() tval = 'application/x-test' r.headers.update({'content-type': tval}) eq_(r.headers.getall('content-type'), [tval]) r.headers.clear() assert not r.headerlist def test_response_copy(): r = Response(app_iter=iter(['a'])) r2 = r.copy() eq_(r.body, 'a') eq_(r2.body, 'a') def test_response_copy_content_md5(): res = Response() res.md5_etag(set_content_md5=True) assert res.content_md5 res2 = res.copy() assert res.content_md5 assert res2.content_md5 eq_(res.content_md5, res2.content_md5) def test_HEAD_closes(): req = Request.blank('/') req.method = 'HEAD' app_iter = io.BytesIO(b'foo') res = req.get_response(Response(app_iter=app_iter)) eq_(res.status_code, 200) eq_(res.body, b'') ok_(app_iter.closed) def test_HEAD_conditional_response_returns_empty_response(): req = Request.blank('/', method='HEAD', if_none_match='none' ) res = Response(conditional_response=True) def start_response(status, headerlist): pass result = res(req.environ, start_response) assert not list(result) def test_HEAD_conditional_response_range_empty_response(): req = Request.blank('/', method = 'HEAD', range=(4,5), ) res = Response('Are we not men?', conditional_response=True) assert req.get_response(res).body == b'' def test_conditional_response_if_none_match_false(): req = Request.blank('/', if_none_match='foo') resp = Response(app_iter=['foo\n'], conditional_response=True, etag='bar') resp = req.get_response(resp) eq_(resp.status_code, 200) def test_conditional_response_if_none_match_true(): req = Request.blank('/', if_none_match='foo') resp = Response(app_iter=['foo\n'], conditional_response=True, etag='foo') resp = req.get_response(resp) eq_(resp.status_code, 304) def test_conditional_response_if_none_match_weak(): req = Request.blank('/', headers={'if-none-match': '"bar"'}) req_weak = Request.blank('/', headers={'if-none-match': 'W/"bar"'}) resp = Response(app_iter=['foo\n'], conditional_response=True, etag='bar') resp_weak = Response(app_iter=['foo\n'], conditional_response=True, headers={'etag': 'W/"bar"'}) for rq in [req, req_weak]: for rp in [resp, resp_weak]: rq.get_response(rp).status_code == 304 r2 = Response(app_iter=['foo\n'], conditional_response=True, headers={'etag': '"foo"'}) r2_weak = Response(app_iter=['foo\n'], conditional_response=True, headers={'etag': 'W/"foo"'}) req_weak.get_response(r2).status_code == 200 req.get_response(r2_weak) == 200 def test_conditional_response_if_modified_since_false(): from datetime import datetime, timedelta req = Request.blank('/', if_modified_since=datetime(2011, 3, 17, 13, 0, 0)) resp = Response(app_iter=['foo\n'], conditional_response=True, last_modified=req.if_modified_since-timedelta(seconds=1)) resp = req.get_response(resp) eq_(resp.status_code, 304) def test_conditional_response_if_modified_since_true(): from datetime import datetime, timedelta req = Request.blank('/', if_modified_since=datetime(2011, 3, 17, 13, 0, 0)) resp = Response(app_iter=['foo\n'], conditional_response=True, last_modified=req.if_modified_since+timedelta(seconds=1)) resp = req.get_response(resp) eq_(resp.status_code, 200) def test_conditional_response_range_not_satisfiable_response(): req = Request.blank('/', range='bytes=100-200') resp = Response(app_iter=['foo\n'], content_length=4, conditional_response=True) resp = req.get_response(resp) eq_(resp.status_code, 416) eq_(resp.content_range.start, None) eq_(resp.content_range.stop, None) eq_(resp.content_range.length, 4) eq_(resp.body, b'Requested range not satisfiable: bytes=100-200') def test_HEAD_conditional_response_range_not_satisfiable_response(): req = Request.blank('/', method='HEAD', range='bytes=100-200') resp = Response(app_iter=['foo\n'], content_length=4, conditional_response=True) resp = req.get_response(resp) eq_(resp.status_code, 416) eq_(resp.content_range.start, None) eq_(resp.content_range.stop, None) eq_(resp.content_range.length, 4) eq_(resp.body, b'') def test_md5_etag(): res = Response() res.body = b"""\ In A.D. 2101 War was beginning. Captain: What happen ? Mechanic: Somebody set up us the bomb. Operator: We get signal. Captain: What ! Operator: Main screen turn on. Captain: It's You !! Cats: How are you gentlemen !! Cats: All your base are belong to us. Cats: You are on the way to destruction. Captain: What you say !! Cats: You have no chance to survive make your time. Cats: HA HA HA HA .... Captain: Take off every 'zig' !! Captain: You know what you doing. Captain: Move 'zig'. Captain: For great justice.""" res.md5_etag() ok_(res.etag) ok_('\n' not in res.etag) eq_(res.etag, 'pN8sSTUrEaPRzmurGptqmw') eq_(res.content_md5, None) def test_md5_etag_set_content_md5(): res = Response() body = b'The quick brown fox jumps over the lazy dog' res.md5_etag(body, set_content_md5=True) eq_(res.content_md5, 'nhB9nTcrtoJr2B01QqQZ1g==') def test_decode_content_defaults_to_identity(): res = Response() res.body = b'There be dragons' res.decode_content() eq_(res.body, b'There be dragons') def test_decode_content_with_deflate(): res = Response() body = b'Hey Hey Hey' # Simulate inflate by chopping the headers off # the gzip encoded data res.body = zlib.compress(body)[2:-4] res.content_encoding = 'deflate' res.decode_content() eq_(res.body, body) eq_(res.content_encoding, None) def test_content_length(): r0 = Response('x'*10, content_length=10) req_head = Request.blank('/', method='HEAD') r1 = req_head.get_response(r0) eq_(r1.status_code, 200) eq_(r1.body, b'') eq_(r1.content_length, 10) req_get = Request.blank('/') r2 = req_get.get_response(r0) eq_(r2.status_code, 200) eq_(r2.body, b'x'*10) eq_(r2.content_length, 10) r3 = Response(app_iter=[b'x']*10) eq_(r3.content_length, None) eq_(r3.body, b'x'*10) eq_(r3.content_length, 10) r4 = Response(app_iter=[b'x']*10, content_length=20) # wrong content_length eq_(r4.content_length, 20) assert_raises(AssertionError, lambda: r4.body) req_range = Request.blank('/', range=(0,5)) r0.conditional_response = True r5 = req_range.get_response(r0) eq_(r5.status_code, 206) eq_(r5.body, b'xxxxx') eq_(r5.content_length, 5) def test_app_iter_range(): req = Request.blank('/', range=(2,5)) for app_iter in [ [b'012345'], [b'0', b'12345'], [b'0', b'1234', b'5'], [b'01', b'2345'], [b'01', b'234', b'5'], [b'012', b'34', b'5'], [b'012', b'3', b'4', b'5'], [b'012', b'3', b'45'], [b'0', b'12', b'34', b'5'], [b'0', b'12', b'345'], ]: r = Response( app_iter=app_iter, content_length=6, conditional_response=True, ) res = req.get_response(r) eq_(list(res.content_range), [2,5,6]) eq_(res.body, b'234', (res.body, app_iter)) def test_app_iter_range_inner_method(): class FakeAppIter: def app_iter_range(self, start, stop): return 'you win', start, stop res = Response(app_iter=FakeAppIter()) eq_(res.app_iter_range(30, 40), ('you win', 30, 40)) def test_content_type_in_headerlist(): # Couldn't manage to clone Response in order to modify class # attributes safely. Shouldn't classes be fresh imported for every # test? default_content_type = Response.default_content_type Response.default_content_type = None try: res = Response(headerlist=[('Content-Type', 'text/html')], charset='utf8') ok_(res._headerlist) eq_(res.charset, 'utf8') finally: Response.default_content_type = default_content_type def test_from_file(): res = Response('test') inp = io.BytesIO(bytes_(str(res))) equal_resp(res, inp) def test_from_file2(): res = Response(app_iter=iter([b'test ', b'body']), content_type='text/plain') inp = io.BytesIO(bytes_(str(res))) equal_resp(res, inp) def test_from_text_file(): res = Response('test') inp = io.StringIO(text_(str(res), 'utf-8')) equal_resp(res, inp) res = Response(app_iter=iter([b'test ', b'body']), content_type='text/plain') inp = io.StringIO(text_(str(res), 'utf-8')) equal_resp(res, inp) def equal_resp(res, inp): res2 = Response.from_file(inp) eq_(res.body, res2.body) eq_(res.headers, res2.headers) def test_from_file_w_leading_space_in_header(): # Make sure the removal of code dealing with leading spaces is safe res1 = Response() file_w_space = io.BytesIO( b'200 OK\n\tContent-Type: text/html; charset=UTF-8') res2 = Response.from_file(file_w_space) eq_(res1.headers, res2.headers) def test_file_bad_header(): file_w_bh = io.BytesIO(b'200 OK\nBad Header') assert_raises(ValueError, Response.from_file, file_w_bh) def test_set_status(): res = Response() res.status = "200" eq_(res.status, "200 OK") assert_raises(TypeError, setattr, res, 'status', float(200)) def test_set_headerlist(): res = Response() # looks like a list res.headerlist = (('Content-Type', 'text/html; charset=UTF-8'),) eq_(res.headerlist, [('Content-Type', 'text/html; charset=UTF-8')]) # has items res.headerlist = {'Content-Type': 'text/html; charset=UTF-8'} eq_(res.headerlist, [('Content-Type', 'text/html; charset=UTF-8')]) del res.headerlist eq_(res.headerlist, []) def test_request_uri_no_script_name(): from webob.response import _request_uri environ = { 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'test.com', } eq_(_request_uri(environ), 'http://test.com/') def test_request_uri_https(): from webob.response import _request_uri environ = { 'wsgi.url_scheme': 'https', 'SERVER_NAME': 'test.com', 'SERVER_PORT': '443', 'SCRIPT_NAME': '/foobar', } eq_(_request_uri(environ), 'https://test.com/foobar') def test_app_iter_range_starts_after_iter_end(): from webob.response import AppIterRange range = AppIterRange(iter([]), start=1, stop=1) eq_(list(range), []) def test_resp_write_app_iter_non_list(): res = Response(app_iter=(b'a', b'b')) eq_(res.content_length, None) res.write(b'c') eq_(res.body, b'abc') eq_(res.content_length, 3) def test_response_file_body_writelines(): from webob.response import ResponseBodyFile res = Response(app_iter=[b'foo']) rbo = ResponseBodyFile(res) rbo.writelines(['bar', 'baz']) eq_(res.app_iter, [b'foo', b'bar', b'baz']) rbo.flush() # noop eq_(res.app_iter, [b'foo', b'bar', b'baz']) def test_response_write_non_str(): res = Response() assert_raises(TypeError, res.write, object()) def test_response_file_body_write_empty_app_iter(): res = Response('foo') res.write('baz') eq_(res.app_iter, [b'foo', b'baz']) def test_response_file_body_write_empty_body(): res = Response('') res.write('baz') eq_(res.app_iter, [b'', b'baz']) def test_response_file_body_close_not_implemented(): rbo = Response().body_file assert_raises(NotImplementedError, rbo.close) def test_response_file_body_repr(): rbo = Response().body_file rbo.response = 'yo' eq_(repr(rbo), "") def test_body_get_is_none(): res = Response() res._app_iter = None assert_raises(TypeError, Response, app_iter=iter(['a']), body="somebody") assert_raises(AttributeError, res.__getattribute__, 'body') def test_body_get_is_unicode_notverylong(): res = Response(app_iter=(text_(b'foo'),)) assert_raises(TypeError, res.__getattribute__, 'body') def test_body_get_is_unicode(): res = Response(app_iter=(['x'] * 51 + [text_(b'x')])) assert_raises(TypeError, res.__getattribute__, 'body') def test_body_set_not_unicode_or_str(): res = Response() assert_raises(TypeError, res.__setattr__, 'body', object()) def test_body_set_unicode(): res = Response() assert_raises(TypeError, res.__setattr__, 'body', text_(b'abc')) def test_body_set_under_body_doesnt_exist(): res = Response('abc') eq_(res.body, b'abc') eq_(res.content_length, 3) def test_body_del(): res = Response('123') del res.body eq_(res.body, b'') eq_(res.content_length, 0) def test_text_get_no_charset(): res = Response(charset=None) assert_raises(AttributeError, res.__getattribute__, 'text') def test_unicode_body(): res = Response() res.charset = 'utf-8' bbody = b'La Pe\xc3\xb1a' # binary string ubody = text_(bbody, 'utf-8') # unicode string res.body = bbody eq_(res.unicode_body, ubody) res.ubody = ubody eq_(res.body, bbody) del res.ubody eq_(res.body, b'') def test_text_get_decode(): res = Response() res.charset = 'utf-8' res.body = b'La Pe\xc3\xb1a' eq_(res.text, text_(b'La Pe\xc3\xb1a', 'utf-8')) def test_text_set_no_charset(): res = Response() res.charset = None assert_raises(AttributeError, res.__setattr__, 'text', 'abc') def test_text_set_not_unicode(): res = Response() res.charset = 'utf-8' assert_raises(TypeError, res.__setattr__, 'text', b'La Pe\xc3\xb1a') def test_text_del(): res = Response('123') del res.text eq_(res.body, b'') eq_(res.content_length, 0) def test_body_file_del(): res = Response() res.body = b'123' eq_(res.content_length, 3) eq_(res.app_iter, [b'123']) del res.body_file eq_(res.body, b'') eq_(res.content_length, 0) def test_write_unicode(): res = Response() res.text = text_(b'La Pe\xc3\xb1a', 'utf-8') res.write(text_(b'a')) eq_(res.text, text_(b'La Pe\xc3\xb1aa', 'utf-8')) def test_write_unicode_no_charset(): res = Response(charset=None) assert_raises(TypeError, res.write, text_(b'a')) def test_write_text(): res = Response() res.body = b'abc' res.write(text_(b'a')) eq_(res.text, 'abca') def test_app_iter_del(): res = Response( content_length=3, app_iter=['123'], ) del res.app_iter eq_(res.body, b'') eq_(res.content_length, None) def test_charset_set_no_content_type_header(): res = Response() res.headers.pop('Content-Type', None) assert_raises(AttributeError, res.__setattr__, 'charset', 'utf-8') def test_charset_del_no_content_type_header(): res = Response() res.headers.pop('Content-Type', None) eq_(res._charset__del(), None) def test_content_type_params_get_no_semicolon_in_content_type_header(): res = Response() res.headers['Content-Type'] = 'foo' eq_(res.content_type_params, {}) def test_content_type_params_get_semicolon_in_content_type_header(): res = Response() res.headers['Content-Type'] = 'foo;encoding=utf-8' eq_(res.content_type_params, {'encoding':'utf-8'}) def test_content_type_params_set_value_dict_empty(): res = Response() res.headers['Content-Type'] = 'foo;bar' res.content_type_params = None eq_(res.headers['Content-Type'], 'foo') def test_content_type_params_set_ok_param_quoting(): res = Response() res.content_type_params = {'a':''} eq_(res.headers['Content-Type'], 'text/html; a=""') def test_set_cookie_overwrite(): res = Response() res.set_cookie('a', '1') res.set_cookie('a', '2', overwrite=True) eq_(res.headerlist[-1], ('Set-Cookie', 'a=2; Path=/')) def test_set_cookie_value_is_None(): res = Response() res.set_cookie('a', None) eq_(res.headerlist[-1][0], 'Set-Cookie') val = [ x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() eq_(val[0], 'Max-Age=0') eq_(val[1], 'Path=/') eq_(val[2], 'a=') assert val[3].startswith('expires') def test_set_cookie_expires_is_None_and_max_age_is_int(): res = Response() res.set_cookie('a', '1', max_age=100) eq_(res.headerlist[-1][0], 'Set-Cookie') val = [ x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() eq_(val[0], 'Max-Age=100') eq_(val[1], 'Path=/') eq_(val[2], 'a=1') assert val[3].startswith('expires') def test_set_cookie_expires_is_None_and_max_age_is_timedelta(): from datetime import timedelta res = Response() res.set_cookie('a', '1', max_age=timedelta(seconds=100)) eq_(res.headerlist[-1][0], 'Set-Cookie') val = [ x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() eq_(val[0], 'Max-Age=100') eq_(val[1], 'Path=/') eq_(val[2], 'a=1') assert val[3].startswith('expires') def test_set_cookie_expires_is_not_None_and_max_age_is_None(): import datetime res = Response() then = datetime.datetime.utcnow() + datetime.timedelta(days=1) res.set_cookie('a', '1', expires=then) eq_(res.headerlist[-1][0], 'Set-Cookie') val = [ x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() ok_(val[0] in ('Max-Age=86399', 'Max-Age=86400')) eq_(val[1], 'Path=/') eq_(val[2], 'a=1') assert val[3].startswith('expires') def test_set_cookie_value_is_unicode(): res = Response() val = text_(b'La Pe\xc3\xb1a', 'utf-8') res.set_cookie('a', val) eq_(res.headerlist[-1], ('Set-Cookie', 'a="La Pe\\303\\261a"; Path=/')) def test_delete_cookie(): res = Response() res.headers['Set-Cookie'] = 'a=2; Path=/' res.delete_cookie('a') eq_(res.headerlist[-1][0], 'Set-Cookie') val = [ x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() eq_(val[0], 'Max-Age=0') eq_(val[1], 'Path=/') eq_(val[2], 'a=') assert val[3].startswith('expires') def test_delete_cookie_with_path(): res = Response() res.headers['Set-Cookie'] = 'a=2; Path=/' res.delete_cookie('a', path='/abc') eq_(res.headerlist[-1][0], 'Set-Cookie') val = [ x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 4 val.sort() eq_(val[0], 'Max-Age=0') eq_(val[1], 'Path=/abc') eq_(val[2], 'a=') assert val[3].startswith('expires') def test_delete_cookie_with_domain(): res = Response() res.headers['Set-Cookie'] = 'a=2; Path=/' res.delete_cookie('a', path='/abc', domain='example.com') eq_(res.headerlist[-1][0], 'Set-Cookie') val = [ x.strip() for x in res.headerlist[-1][1].split(';')] assert len(val) == 5 val.sort() eq_(val[0], 'Domain=example.com') eq_(val[1], 'Max-Age=0') eq_(val[2], 'Path=/abc') eq_(val[3], 'a=') assert val[4].startswith('expires') def test_unset_cookie_not_existing_and_not_strict(): res = Response() res.unset_cookie('a', strict=False) # no exception def test_unset_cookie_not_existing_and_strict(): res = Response() assert_raises(KeyError, res.unset_cookie, 'a') def test_unset_cookie_key_in_cookies(): res = Response() res.headers.add('Set-Cookie', 'a=2; Path=/') res.headers.add('Set-Cookie', 'b=3; Path=/') res.unset_cookie('a') eq_(res.headers.getall('Set-Cookie'), ['b=3; Path=/']) res.unset_cookie(text_('b')) eq_(res.headers.getall('Set-Cookie'), []) def test_merge_cookies_no_set_cookie(): res = Response() result = res.merge_cookies('abc') eq_(result, 'abc') def test_merge_cookies_resp_is_Response(): inner_res = Response() res = Response() res.set_cookie('a', '1') result = res.merge_cookies(inner_res) eq_(result.headers.getall('Set-Cookie'), ['a=1; Path=/']) def test_merge_cookies_resp_is_wsgi_callable(): L = [] def dummy_wsgi_callable(environ, start_response): L.append((environ, start_response)) return 'abc' res = Response() res.set_cookie('a', '1') wsgiapp = res.merge_cookies(dummy_wsgi_callable) environ = {} def dummy_start_response(status, headers, exc_info=None): eq_(headers, [('Set-Cookie', 'a=1; Path=/')]) result = wsgiapp(environ, dummy_start_response) assert result == 'abc' assert len(L) == 1 L[0][1]('200 OK', []) # invoke dummy_start_response assertion def test_body_get_body_is_None_len_app_iter_is_zero(): res = Response() res._app_iter = io.BytesIO() res._body = None result = res.body eq_(result, b'') def test_cache_control_get(): res = Response() eq_(repr(res.cache_control), "") eq_(res.cache_control.max_age, None) def test_location(): res = Response() res.location = '/test.html' eq_(res.location, '/test.html') req = Request.blank('/') eq_(req.get_response(res).location, 'http://localhost/test.html') res.location = '/test2.html' eq_(req.get_response(res).location, 'http://localhost/test2.html') def test_request_uri_http(): # covers webob/response.py:1152 from webob.response import _request_uri environ = { 'wsgi.url_scheme': 'http', 'SERVER_NAME': 'test.com', 'SERVER_PORT': '80', 'SCRIPT_NAME': '/foobar', } eq_(_request_uri(environ), 'http://test.com/foobar') def test_request_uri_no_script_name2(): # covers webob/response.py:1160 # There is a test_request_uri_no_script_name in test_response.py, but it # sets SCRIPT_NAME. from webob.response import _request_uri environ = { 'wsgi.url_scheme': 'http', 'HTTP_HOST': 'test.com', 'PATH_INFO': '/foobar', } eq_(_request_uri(environ), 'http://test.com/foobar') def test_cache_control_object_max_age_ten(): res = Response() res.cache_control.max_age = 10 eq_(repr(res.cache_control), "") eq_(res.headers['cache-control'], 'max-age=10') def test_cache_control_set_object_error(): res = Response() assert_raises(AttributeError, setattr, res.cache_control, 'max_stale', 10) def test_cache_expires_set(): res = Response() res.cache_expires = True eq_(repr(res.cache_control), "") def test_status_code_set(): res = Response() res.status_code = 400 eq_(res._status, '400 Bad Request') res.status_int = 404 eq_(res._status, '404 Not Found') def test_cache_control_set_dict(): res = Response() res.cache_control = {'a':'b'} eq_(repr(res.cache_control), "") def test_cache_control_set_None(): res = Response() res.cache_control = None eq_(repr(res.cache_control), "") def test_cache_control_set_unicode(): res = Response() res.cache_control = text_(b'abc') eq_(repr(res.cache_control), "") def test_cache_control_set_control_obj_is_not_None(): class DummyCacheControl(object): def __init__(self): self.header_value = 1 self.properties = {'bleh':1} res = Response() res._cache_control_obj = DummyCacheControl() res.cache_control = {} eq_(res.cache_control.properties, {}) def test_cache_control_del(): res = Response() del res.cache_control eq_(repr(res.cache_control), "") def test_body_file_get(): res = Response() result = res.body_file from webob.response import ResponseBodyFile eq_(result.__class__, ResponseBodyFile) def test_body_file_write_no_charset(): res = Response assert_raises(TypeError, res.write, text_('foo')) def test_body_file_write_unicode_encodes(): s = text_(b'La Pe\xc3\xb1a', 'utf-8') res = Response() res.write(s) eq_(res.app_iter, [b'', b'La Pe\xc3\xb1a']) def test_repr(): res = Response() ok_(repr(res).endswith('200 OK>')) def test_cache_expires_set_timedelta(): res = Response() from datetime import timedelta delta = timedelta(seconds=60) res.cache_expires(seconds=delta) eq_(res.cache_control.max_age, 60) def test_cache_expires_set_int(): res = Response() res.cache_expires(seconds=60) eq_(res.cache_control.max_age, 60) def test_cache_expires_set_None(): res = Response() res.cache_expires(seconds=None, a=1) eq_(res.cache_control.a, 1) def test_cache_expires_set_zero(): res = Response() res.cache_expires(seconds=0) eq_(res.cache_control.no_store, True) eq_(res.cache_control.no_cache, '*') eq_(res.cache_control.must_revalidate, True) eq_(res.cache_control.max_age, 0) eq_(res.cache_control.post_check, 0) def test_encode_content_unknown(): res = Response() assert_raises(AssertionError, res.encode_content, 'badencoding') def test_encode_content_identity(): res = Response() result = res.encode_content('identity') eq_(result, None) def test_encode_content_gzip_already_gzipped(): res = Response() res.content_encoding = 'gzip' result = res.encode_content('gzip') eq_(result, None) def test_encode_content_gzip_notyet_gzipped(): res = Response() res.app_iter = io.BytesIO(b'foo') result = res.encode_content('gzip') eq_(result, None) eq_(res.content_length, 23) eq_(res.app_iter, [ b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff', b'K\xcb\xcf\x07\x00', b'!es\x8c\x03\x00\x00\x00' ]) def test_encode_content_gzip_notyet_gzipped_lazy(): res = Response() res.app_iter = io.BytesIO(b'foo') result = res.encode_content('gzip', lazy=True) eq_(result, None) eq_(res.content_length, None) eq_(list(res.app_iter), [ b'\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff', b'K\xcb\xcf\x07\x00', b'!es\x8c\x03\x00\x00\x00' ]) def test_encode_content_gzip_buffer_coverage(): #this test is to provide 100% coverage of #.Response.encode_content was necessary in order to get # request https://github.com/Pylons/webob/pull/85 into upstream res = Response() DATA = b"abcdefghijklmnopqrstuvwxyz0123456789" * 1000000 res.app_iter = io.BytesIO(DATA) res.encode_content('gzip') result = list(res.app_iter) assert len(b"".join(result)) < len(DATA) def test_decode_content_identity(): res = Response() res.content_encoding = 'identity' result = res.decode_content() eq_(result, None) def test_decode_content_weird(): res = Response() res.content_encoding = 'weird' assert_raises(ValueError, res.decode_content) def test_decode_content_gzip(): from gzip import GzipFile io_ = io.BytesIO() gzip_f = GzipFile(filename='', mode='w', fileobj=io_) gzip_f.write(b'abc') gzip_f.close() body = io_.getvalue() res = Response() res.content_encoding = 'gzip' res.body = body res.decode_content() eq_(res.body, b'abc') def test__abs_headerlist_location_with_scheme(): res = Response() res.content_encoding = 'gzip' res.headerlist = [('Location', 'http:')] result = res._abs_headerlist({}) eq_(result, [('Location', 'http:')]) def test__abs_headerlist_location_no_scheme(): res = Response() res.content_encoding = 'gzip' res.headerlist = [('Location', '/abc')] result = res._abs_headerlist({'wsgi.url_scheme':'http', 'HTTP_HOST':'example.com:80'}) eq_(result, [('Location', 'http://example.com/abc')]) def test_response_set_body_file1(): data = b'abc' file = io.BytesIO(data) r = Response(body_file=file) assert r.body == data def test_response_set_body_file2(): data = b'abcdef'*1024 file = io.BytesIO(data) r = Response(body_file=file) assert r.body == data def test_response_json_body(): r = Response(json_body={'a': 1}) assert r.body == b'{"a":1}', repr(r.body) assert r.content_type == 'application/json' r = Response() r.json_body = {"b": 1} assert r.content_type == 'text/html' del r.json_body assert r.body == b'' def test_cache_expires_set_zero_then_nonzero(): res = Response() res.cache_expires(seconds=0) res.cache_expires(seconds=1) eq_(res.pragma, None) ok_(not res.cache_control.no_cache) ok_(not res.cache_control.no_store) ok_(not res.cache_control.must_revalidate) eq_(res.cache_control.max_age, 1) WebOb-1.3.1/tests/test_cachecontrol.py0000664000175000017500000002032012123261311020514 0ustar chrismchrism00000000000000from nose.tools import eq_ from nose.tools import raises import unittest def test_cache_control_object_max_age_None(): from webob.cachecontrol import CacheControl cc = CacheControl({}, 'a') cc.properties['max-age'] = None eq_(cc.max_age, -1) class TestUpdateDict(unittest.TestCase): def setUp(self): self.call_queue = [] def callback(args): self.call_queue.append("Called with: %s" % repr(args)) self.callback = callback def make_one(self, callback): from webob.cachecontrol import UpdateDict ud = UpdateDict() ud.updated = callback return ud def test_clear(self): newone = self.make_one(self.callback) newone['first'] = 1 assert len(newone) == 1 newone.clear() assert len(newone) == 0 def test_update(self): newone = self.make_one(self.callback) d = {'one' : 1 } newone.update(d) assert newone == d def test_set_delete(self): newone = self.make_one(self.callback) newone['first'] = 1 assert len(self.call_queue) == 1 assert self.call_queue[-1] == "Called with: {'first': 1}" del newone['first'] assert len(self.call_queue) == 2 assert self.call_queue[-1] == 'Called with: {}' def test_setdefault(self): newone = self.make_one(self.callback) assert newone.setdefault('haters', 'gonna-hate') == 'gonna-hate' assert len(self.call_queue) == 1 assert self.call_queue[-1] == "Called with: {'haters': 'gonna-hate'}", self.call_queue[-1] # no effect if failobj is not set assert newone.setdefault('haters', 'gonna-love') == 'gonna-hate' assert len(self.call_queue) == 1 def test_pop(self): newone = self.make_one(self.callback) newone['first'] = 1 newone.pop('first') assert len(self.call_queue) == 2 assert self.call_queue[-1] == 'Called with: {}', self.call_queue[-1] def test_popitem(self): newone = self.make_one(self.callback) newone['first'] = 1 assert newone.popitem() == ('first', 1) assert len(self.call_queue) == 2 assert self.call_queue[-1] == 'Called with: {}', self.call_queue[-1] def test_callback_args(self): assert True #assert False class TestExistProp(unittest.TestCase): """ Test webob.cachecontrol.exists_property """ def setUp(self): pass def make_one(self): from webob.cachecontrol import exists_property class Dummy(object): properties = dict(prop=1) type = 'dummy' prop = exists_property('prop', 'dummy') badprop = exists_property('badprop', 'big_dummy') return Dummy def test_get_on_class(self): from webob.cachecontrol import exists_property Dummy = self.make_one() assert isinstance(Dummy.prop, exists_property), Dummy.prop def test_get_on_instance(self): obj = self.make_one()() assert obj.prop is True @raises(AttributeError) def test_type_mismatch_raise(self): obj = self.make_one()() obj.badprop = True def test_set_w_value(self): obj = self.make_one()() obj.prop = True assert obj.prop is True assert obj.properties['prop'] is None def test_del_value(self): obj = self.make_one()() del obj.prop assert not 'prop' in obj.properties class TestValueProp(unittest.TestCase): """ Test webob.cachecontrol.exists_property """ def setUp(self): pass def make_one(self): from webob.cachecontrol import value_property class Dummy(object): properties = dict(prop=1) type = 'dummy' prop = value_property('prop', 'dummy') badprop = value_property('badprop', 'big_dummy') return Dummy def test_get_on_class(self): from webob.cachecontrol import value_property Dummy = self.make_one() assert isinstance(Dummy.prop, value_property), Dummy.prop def test_get_on_instance(self): dummy = self.make_one()() assert dummy.prop, dummy.prop #assert isinstance(Dummy.prop, value_property), Dummy.prop def test_set_on_instance(self): dummy = self.make_one()() dummy.prop = "new" assert dummy.prop == "new", dummy.prop assert dummy.properties['prop'] == "new", dict(dummy.properties) def test_set_on_instance_bad_attribute(self): dummy = self.make_one()() dummy.prop = "new" assert dummy.prop == "new", dummy.prop assert dummy.properties['prop'] == "new", dict(dummy.properties) def test_set_wrong_type(self): from webob.cachecontrol import value_property class Dummy(object): properties = dict(prop=1, type='fail') type = 'dummy' prop = value_property('prop', 'dummy', type='failingtype') dummy = Dummy() def assign(): dummy.prop = 'foo' self.assertRaises(AttributeError, assign) def test_set_type_true(self): dummy = self.make_one()() dummy.prop = True self.assertEqual(dummy.prop, None) def test_set_on_instance_w_default(self): dummy = self.make_one()() dummy.prop = "dummy" assert dummy.prop == "dummy", dummy.prop #@@ this probably needs more tests def test_del(self): dummy = self.make_one()() dummy.prop = 'Ian Bicking likes to skip' del dummy.prop assert dummy.prop == "dummy", dummy.prop def test_copy_cc(): from webob.cachecontrol import CacheControl cc = CacheControl({'header':'%', "msg":'arewerichyet?'}, 'request') cc2 = cc.copy() assert cc.properties is not cc2.properties assert cc.type is cc2.type # 212 def test_serialize_cache_control_emptydict(): from webob.cachecontrol import serialize_cache_control result = serialize_cache_control(dict()) assert result == '' def test_serialize_cache_control_cache_control_object(): from webob.cachecontrol import serialize_cache_control, CacheControl result = serialize_cache_control(CacheControl({}, 'request')) assert result == '' def test_serialize_cache_control_object_with_headers(): from webob.cachecontrol import serialize_cache_control, CacheControl result = serialize_cache_control(CacheControl({'header':'a'}, 'request')) assert result == 'header=a' def test_serialize_cache_control_value_is_None(): from webob.cachecontrol import serialize_cache_control, CacheControl result = serialize_cache_control(CacheControl({'header':None}, 'request')) assert result == 'header' def test_serialize_cache_control_value_needs_quote(): from webob.cachecontrol import serialize_cache_control, CacheControl result = serialize_cache_control(CacheControl({'header':'""'}, 'request')) assert result == 'header=""""' class TestCacheControl(unittest.TestCase): def make_one(self, props, typ): from webob.cachecontrol import CacheControl return CacheControl(props, typ) def test_ctor(self): cc = self.make_one({'a':1}, 'typ') self.assertEqual(cc.properties, {'a':1}) self.assertEqual(cc.type, 'typ') def test_parse(self): from webob.cachecontrol import CacheControl cc = CacheControl.parse("public, max-age=315360000") self.assertEqual(type(cc), CacheControl) self.assertEqual(cc.max_age, 315360000) self.assertEqual(cc.public, True) def test_parse_updates_to(self): from webob.cachecontrol import CacheControl def foo(arg): return { 'a' : 1 } cc = CacheControl.parse("public, max-age=315360000", updates_to=foo) self.assertEqual(type(cc), CacheControl) self.assertEqual(cc.max_age, 315360000) def test_parse_valueerror_int(self): from webob.cachecontrol import CacheControl def foo(arg): return { 'a' : 1 } cc = CacheControl.parse("public, max-age=abc") self.assertEqual(type(cc), CacheControl) self.assertEqual(cc.max_age, 'abc') def test_repr(self): cc = self.make_one({'a':'1'}, 'typ') result = repr(cc) self.assertEqual(result, "") WebOb-1.3.1/tests/test_client.py0000664000175000017500000002232212023145137017340 0ustar chrismchrism00000000000000import unittest import io import socket class TestSendRequest(unittest.TestCase): def _getTargetClass(self): from webob.client import SendRequest return SendRequest def _makeOne(self, **kw): cls = self._getTargetClass() return cls(**kw) def _makeEnviron(self, extra=None): environ = { 'wsgi.url_scheme':'http', 'SERVER_NAME':'localhost', 'HTTP_HOST':'localhost:80', 'SERVER_PORT':'80', 'wsgi.input':io.BytesIO(), 'CONTENT_LENGTH':0, 'REQUEST_METHOD':'GET', } if extra is not None: environ.update(extra) return environ def test___call___unknown_scheme(self): environ = self._makeEnviron({'wsgi.url_scheme':'abc'}) inst = self._makeOne() self.assertRaises(ValueError, inst, environ, None) def test___call___gardenpath(self): environ = self._makeEnviron() response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) def test___call___no_servername_no_http_host(self): environ = self._makeEnviron() del environ['SERVER_NAME'] del environ['HTTP_HOST'] response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) self.assertRaises(ValueError, inst, environ, None) def test___call___no_servername_colon_not_in_host_http(self): environ = self._makeEnviron() del environ['SERVER_NAME'] environ['HTTP_HOST'] = 'localhost' response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) self.assertEqual(environ['SERVER_NAME'], 'localhost') self.assertEqual(environ['SERVER_PORT'], '80') def test___call___no_servername_colon_not_in_host_https(self): environ = self._makeEnviron() del environ['SERVER_NAME'] environ['HTTP_HOST'] = 'localhost' environ['wsgi.url_scheme'] = 'https' response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPSConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) self.assertEqual(environ['SERVER_NAME'], 'localhost') self.assertEqual(environ['SERVER_PORT'], '443') def test___call___no_content_length(self): environ = self._makeEnviron() del environ['CONTENT_LENGTH'] response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) def test___call___with_webob_client_timeout_and_timeout_supported(self): environ = self._makeEnviron() environ['webob.client.timeout'] = 10 response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) self.assertEqual(conn_factory.kw, {'timeout':10}) def test___call___bad_content_length(self): environ = self._makeEnviron({'CONTENT_LENGTH':'abc'}) response = DummyResponse('msg') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) def test___call___with_socket_timeout(self): environ = self._makeEnviron() response = socket.timeout() response.msg = 'msg' conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '504 Gateway Timeout') inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertTrue(list(iterable)[0].startswith(b'504')) def test___call___with_socket_error_neg2(self): environ = self._makeEnviron() response = socket.error(-2) conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '502 Bad Gateway') inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertTrue(list(iterable)[0].startswith(b'502')) def test___call___with_socket_error_ENODATA(self): import errno environ = self._makeEnviron() if not hasattr(errno, 'ENODATA'): # no ENODATA on win return response = socket.error(errno.ENODATA) conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '502 Bad Gateway') inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertTrue(list(iterable)[0].startswith(b'502')) def test___call___with_socket_error_unknown(self): environ = self._makeEnviron() response = socket.error('nope') conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '502 Bad Gateway') inst.start_response_called = True self.assertRaises(socket.error, inst, environ, start_response) def test___call___nolength(self): environ = self._makeEnviron() response = DummyResponse('msg', None) conn_factory = DummyConnectionFactory(response) inst = self._makeOne(HTTPConnection=conn_factory) def start_response(status, headers): self.assertEqual(status, '200 OK') self.assertEqual(headers, []) inst.start_response_called = True iterable = inst(environ, start_response) self.assertTrue(inst.start_response_called) self.assertEqual(list(iterable), [b'foo']) self.assertEqual(response.length, None) class DummyMessage(object): def __init__(self, msg): self.msg = msg self.headers = self._headers = {} class DummyResponse(object): def __init__(self, msg, headerval='10'): self.msg = DummyMessage(msg) self.status = '200' self.reason = 'OK' self.headerval = headerval def getheader(self, name): return self.headerval def read(self, length=None): self.length = length return b'foo' class DummyConnectionFactory(object): def __init__(self, result=None): self.result = result self.closed = False def __call__(self, hostport, **kw): self.hostport = hostport self.kw = kw self.request = DummyRequestFactory(hostport, **kw) return self def getresponse(self): if isinstance(self.result, Exception): raise self.result return self.result def close(self): self.closed = True class DummyRequestFactory(object): def __init__(self, hostport, **kw): self.hostport = hostport self.kw = kw def __call__(self, method, path, body, headers): return self WebOb-1.3.1/tests/test_compat.py0000664000175000017500000000322512123261311017340 0ustar chrismchrism00000000000000import unittest from webob.compat import text_type class text_Tests(unittest.TestCase): def _callFUT(self, *arg, **kw): from webob.compat import text_ return text_(*arg, **kw) def test_binary(self): result = self._callFUT(b'123') self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_type(b'123', 'ascii')) def test_binary_alternate_decoding(self): result = self._callFUT(b'La Pe\xc3\xb1a', 'utf-8') self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_type(b'La Pe\xc3\xb1a', 'utf-8')) def test_binary_decoding_error(self): self.assertRaises(UnicodeDecodeError, self._callFUT, b'\xff', 'utf-8') def test_text(self): result = self._callFUT(text_type(b'123', 'ascii')) self.assertTrue(isinstance(result, text_type)) self.assertEqual(result, text_type(b'123', 'ascii')) class bytes_Tests(unittest.TestCase): def _callFUT(self, *arg, **kw): from webob.compat import bytes_ return bytes_(*arg, **kw) def test_binary(self): result = self._callFUT(b'123') self.assertTrue(isinstance(result, bytes)) self.assertEqual(result, b'123') def test_text(self): val = text_type(b'123', 'ascii') result = self._callFUT(val) self.assertTrue(isinstance(result, bytes)) self.assertEqual(result, b'123') def test_text_alternate_encoding(self): val = text_type(b'La Pe\xc3\xb1a', 'utf-8') result = self._callFUT(val, 'utf-8') self.assertTrue(isinstance(result, bytes)) self.assertEqual(result, b'La Pe\xc3\xb1a') WebOb-1.3.1/tests/test_exc.py0000664000175000017500000002506511752315637016664 0ustar chrismchrism00000000000000from webob.request import Request from webob.dec import wsgify from webob.exc import no_escape from webob.exc import strip_tags from webob.exc import HTTPException from webob.exc import WSGIHTTPException from webob.exc import _HTTPMove from webob.exc import HTTPMethodNotAllowed from webob.exc import HTTPExceptionMiddleware from webob.exc import status_map from nose.tools import eq_, ok_, assert_equal, assert_raises @wsgify def method_not_allowed_app(req): if req.method != 'GET': raise HTTPMethodNotAllowed() return 'hello!' def test_noescape_null(): assert_equal(no_escape(None), '') def test_noescape_not_basestring(): assert_equal(no_escape(42), '42') def test_noescape_unicode(): class DummyUnicodeObject(object): def __unicode__(self): return '42' duo = DummyUnicodeObject() assert_equal(no_escape(duo), '42') def test_strip_tags_empty(): assert_equal(strip_tags(''), '') def test_strip_tags_newline_to_space(): assert_equal(strip_tags('a\nb'), 'a b') def test_strip_tags_zaps_carriage_return(): assert_equal(strip_tags('a\rb'), 'ab') def test_strip_tags_br_to_newline(): assert_equal(strip_tags('a
b'), 'a\nb') def test_strip_tags_zaps_comments(): assert_equal(strip_tags('a'), 'ab') def test_strip_tags_zaps_tags(): assert_equal(strip_tags('foobaz'), 'foobaz') def test_HTTPException(): import warnings _called = [] _result = object() def _response(environ, start_response): _called.append((environ, start_response)) return _result environ = {} start_response = object() exc = HTTPException('testing', _response) ok_(exc.wsgi_response is _response) with warnings.catch_warnings(record=True) as w: warnings.simplefilter("always") assert(exc.exception is exc) assert(len(w) == 1) result = exc(environ, start_response) ok_(result is result) assert_equal(_called, [(environ, start_response)]) def test_exception_with_unicode_data(): req = Request.blank('/', method='POST') res = req.get_response(method_not_allowed_app) assert res.status_code == 405 def test_WSGIHTTPException_headers(): exc = WSGIHTTPException(headers=[('Set-Cookie', 'a=1'), ('Set-Cookie', 'a=2')]) mixed = exc.headers.mixed() assert mixed['set-cookie'] == ['a=1', 'a=2'] def test_WSGIHTTPException_w_body_template(): from string import Template TEMPLATE = '$foo: $bar' exc = WSGIHTTPException(body_template = TEMPLATE) assert_equal(exc.body_template, TEMPLATE) ok_(isinstance(exc.body_template_obj, Template)) eq_(exc.body_template_obj.substitute({'foo': 'FOO', 'bar': 'BAR'}), 'FOO: BAR') def test_WSGIHTTPException_w_empty_body(): class EmptyOnly(WSGIHTTPException): empty_body = True exc = EmptyOnly(content_type='text/plain', content_length=234) ok_('content_type' not in exc.__dict__) ok_('content_length' not in exc.__dict__) def test_WSGIHTTPException___str__(): exc1 = WSGIHTTPException(detail='Detail') eq_(str(exc1), 'Detail') class Explain(WSGIHTTPException): explanation = 'Explanation' eq_(str(Explain()), 'Explanation') def test_WSGIHTTPException_plain_body_no_comment(): class Explain(WSGIHTTPException): code = '999' title = 'Testing' explanation = 'Explanation' exc = Explain(detail='Detail') eq_(exc.plain_body({}), '999 Testing\n\nExplanation\n\n Detail ') def test_WSGIHTTPException_html_body_w_comment(): class Explain(WSGIHTTPException): code = '999' title = 'Testing' explanation = 'Explanation' exc = Explain(detail='Detail', comment='Comment') eq_(exc.html_body({}), '\n' ' \n' ' 999 Testing\n' ' \n' ' \n' '

999 Testing

\n' ' Explanation

\n' 'Detail\n' '\n\n' ' \n' '' ) def test_WSGIHTTPException_generate_response(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'PUT', 'HTTP_ACCEPT': 'text/html' } excep = WSGIHTTPException() assert_equal( excep(environ,start_response), [ b'\n' b' \n' b' None None\n' b' \n' b' \n' b'

None None

\n' b'

\n' b'\n' b'\n\n' b' \n' b'' ] ) def test_WSGIHTTPException_call_w_body(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'PUT' } excep = WSGIHTTPException() excep.body = b'test' assert_equal( excep(environ,start_response), [b'test'] ) def test_WSGIHTTPException_wsgi_response(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD' } excep = WSGIHTTPException() assert_equal( excep.wsgi_response(environ,start_response), [] ) def test_WSGIHTTPException_exception_newstyle(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD' } excep = WSGIHTTPException() from webob import exc exc.newstyle_exceptions = True assert_equal( excep(environ,start_response), [] ) def test_WSGIHTTPException_exception_no_newstyle(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD' } excep = WSGIHTTPException() from webob import exc exc.newstyle_exceptions = False assert_equal( excep(environ,start_response), [] ) def test_HTTPOk_head_of_proxied_head(): # first set up a response to a HEAD request HELLO_WORLD = "Hi!\n" CONTENT_TYPE = "application/hello" def head_app(environ, start_response): """An application object that understands HEAD""" status = '200 OK' response_headers = [('Content-Type', CONTENT_TYPE), ('Content-Length', len(HELLO_WORLD))] start_response(status, response_headers) if environ['REQUEST_METHOD'] == 'HEAD': return [] else: return [HELLO_WORLD] def verify_response(resp, description): assert_equal(resp.content_type, CONTENT_TYPE, description) assert_equal(resp.content_length, len(HELLO_WORLD), description) assert_equal(resp.body, b'', description) req = Request.blank('/', method='HEAD') resp1 = req.get_response(head_app) verify_response(resp1, "first response") # Copy the response like a proxy server would. # Copying an empty body has set content_length # so copy the headers only afterwards. resp2 = status_map[resp1.status_int](request=req) resp2.body = resp1.body resp2.headerlist = resp1.headerlist verify_response(resp2, "copied response") # evaluate it again resp3 = req.get_response(resp2) verify_response(resp3, "evaluated copy") def test_HTTPMove(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/', } m = _HTTPMove() assert_equal( m( environ, start_response ), [] ) def test_HTTPMove_location_not_none(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/', } m = _HTTPMove(location='http://example.com') assert_equal( m( environ, start_response ), [] ) def test_HTTPMove_add_slash_and_location(): def start_response(status, headers, exc_info=None): pass assert_raises( TypeError, _HTTPMove, location='http://example.com', add_slash=True ) def test_HTTPMove_call_add_slash(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD', 'PATH_INFO': '/', } m = _HTTPMove() m.add_slash = True assert_equal( m( environ, start_response ), [] ) def test_HTTPMove_call_query_string(): def start_response(status, headers, exc_info=None): pass environ = { 'wsgi.url_scheme': 'HTTP', 'SERVER_NAME': 'localhost', 'SERVER_PORT': '80', 'REQUEST_METHOD': 'HEAD' } m = _HTTPMove() m.add_slash = True environ[ 'QUERY_STRING' ] = 'querystring' environ['PATH_INFO'] = '/' assert_equal( m( environ, start_response ), [] ) def test_HTTPExceptionMiddleware_ok(): def app( environ, start_response ): return '123' application = app m = HTTPExceptionMiddleware(application) environ = {} start_response = None res = m( environ, start_response ) assert_equal( res, '123' ) def test_HTTPExceptionMiddleware_exception(): def wsgi_response( environ, start_response): return '123' def app( environ, start_response ): raise HTTPException( None, wsgi_response ) application = app m = HTTPExceptionMiddleware(application) environ = {} start_response = None res = m( environ, start_response ) assert_equal( res, '123' ) def test_HTTPExceptionMiddleware_exception_exc_info_none(): class DummySys: def exc_info(self): return None def wsgi_response( environ, start_response): return start_response('200 OK', [], exc_info=None) def app( environ, start_response ): raise HTTPException( None, wsgi_response ) application = app m = HTTPExceptionMiddleware(application) environ = {} def start_response(status, headers, exc_info): pass try: from webob import exc old_sys = exc.sys sys = DummySys() res = m( environ, start_response ) assert_equal( res, None ) finally: exc.sys = old_sys WebOb-1.3.1/tests/test_byterange.py0000664000175000017500000000670112023145137020045 0ustar chrismchrism00000000000000from webob.byterange import Range from webob.byterange import ContentRange from webob.byterange import _is_content_range_valid from nose.tools import assert_true, assert_false, eq_, assert_raises # Range class def test_not_satisfiable(): range = Range.parse('bytes=-100') assert range.range_for_length(50) is None range = Range.parse('bytes=100-') assert range.range_for_length(50) is None def test_range_parse(): assert isinstance(Range.parse('bytes=0-99'), Range) assert isinstance(Range.parse('BYTES=0-99'), Range) assert isinstance(Range.parse('bytes = 0-99'), Range) assert isinstance(Range.parse('bytes=0 - 102'), Range) assert Range.parse('bytes=10-5') is None assert Range.parse('bytes 5-10') is None assert Range.parse('words=10-5') is None def test_range_content_range_length_none(): range = Range(0, 100) eq_(range.content_range(None), None) assert isinstance(range.content_range(1), ContentRange) eq_(tuple(range.content_range(1)), (0,1,1)) eq_(tuple(range.content_range(200)), (0,100,200)) def test_range_for_length_end_is_none(): # End is None range = Range(0, None) eq_(range.range_for_length(100), (0,100)) def test_range_for_length_end_is_none_negative_start(): # End is None and start is negative range = Range(-5, None) eq_(range.range_for_length(100), (95,100)) def test_range_start_none(): # Start is None range = Range(None, 99) eq_(range.range_for_length(100), None) def test_range_str_end_none(): range = Range(0, None) eq_(str(range), 'bytes=0-') def test_range_str_end_none_negative_start(): range = Range(-5, None) eq_(str(range), 'bytes=-5') def test_range_str_1(): range = Range(0, 100) eq_(str(range), 'bytes=0-99') def test_range_repr(): range = Range(0, 99) assert_true(range.__repr__(), '') # ContentRange class def test_contentrange_bad_input(): assert_raises(ValueError, ContentRange, None, 99, None) def test_contentrange_repr(): contentrange = ContentRange(0, 99, 100) assert_true(repr(contentrange), '') def test_contentrange_str(): contentrange = ContentRange(0, 99, None) eq_(str(contentrange), 'bytes 0-98/*') contentrange = ContentRange(None, None, 100) eq_(str(contentrange), 'bytes */100') def test_contentrange_iter(): contentrange = ContentRange(0, 99, 100) assert_true(type(contentrange.__iter__()), iter) assert_true(ContentRange.parse('bytes 0-99/100').__class__, ContentRange) eq_(ContentRange.parse(None), None) eq_(ContentRange.parse('0-99 100'), None) eq_(ContentRange.parse('bytes 0-99 100'), None) eq_(ContentRange.parse('bytes 0-99/xxx'), None) eq_(ContentRange.parse('bytes 0 99/100'), None) eq_(ContentRange.parse('bytes */100').__class__, ContentRange) eq_(ContentRange.parse('bytes A-99/100'), None) eq_(ContentRange.parse('bytes 0-B/100'), None) eq_(ContentRange.parse('bytes 99-0/100'), None) eq_(ContentRange.parse('bytes 0 99/*'), None) # _is_content_range_valid function def test_is_content_range_valid(): assert not _is_content_range_valid( None, 99, 90) assert not _is_content_range_valid( 99, None, 90) assert _is_content_range_valid(None, None, 90) assert not _is_content_range_valid(None, 99, 90) assert _is_content_range_valid(0, 99, None) assert not _is_content_range_valid(0, 99, 90, response=True) assert _is_content_range_valid(0, 99, 90) WebOb-1.3.1/tests/test_etag.py0000664000175000017500000001322012123261311016771 0ustar chrismchrism00000000000000import unittest from webob import Response from webob.etag import ETagMatcher, IfRange, etag_property, ETagMatcher class etag_propertyTests(unittest.TestCase): def _makeDummyRequest(self, **kw): """ Return a DummyRequest object with attrs from kwargs. Use like: dr = _makeDummyRequest(environment={'userid': 'johngalt'}) Then you can: uid = dr.environment.get('userid', 'SomeDefault') """ class Dummy(object): def __init__(self, **kwargs): self.__dict__.update(**kwargs) d = Dummy(**kw) return d def test_fget_missing_key(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={}) self.assertEqual(ep.fget(req), "DEFAULT") def test_fget_found_key(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={'KEY':'"VALUE"'}) res = ep.fget(req) self.assertEqual(res.etags, ['VALUE']) def test_fget_star_key(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={'KEY':'*'}) res = ep.fget(req) import webob.etag self.assertEqual(type(res), webob.etag._AnyETag) self.assertEqual(res.__dict__, {}) def test_fset_None(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={'KEY':'*'}) res = ep.fset(req, None) self.assertEqual(res, None) def test_fset_not_None(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={'KEY':'OLDVAL'}) res = ep.fset(req, "NEWVAL") self.assertEqual(res, None) self.assertEqual(req.environ['KEY'], 'NEWVAL') def test_fedl(self): ep = etag_property("KEY", "DEFAULT", "RFC_SECTION") req = self._makeDummyRequest(environ={'KEY':'VAL', 'QUAY':'VALYOU'}) res = ep.fdel(req) self.assertEqual(res, None) self.assertFalse('KEY' in req.environ) self.assertEqual(req.environ['QUAY'], 'VALYOU') class AnyETagTests(unittest.TestCase): def _getTargetClass(self): from webob.etag import _AnyETag return _AnyETag def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) def test___repr__(self): etag = self._makeOne() self.assertEqual(etag.__repr__(), '') def test___nonzero__(self): etag = self._makeOne() self.assertEqual(etag.__nonzero__(), False) def test___contains__something(self): etag = self._makeOne() self.assertEqual('anything' in etag, True) def test_weak_match_something(self): etag = self._makeOne() self.assertRaises(DeprecationWarning, etag.weak_match, 'anything') def test___str__(self): etag = self._makeOne() self.assertEqual(str(etag), '*') class NoETagTests(unittest.TestCase): def _getTargetClass(self): from webob.etag import _NoETag return _NoETag def _makeOne(self, *args, **kw): return self._getTargetClass()(*args, **kw) def test___repr__(self): etag = self._makeOne() self.assertEqual(etag.__repr__(), '') def test___nonzero__(self): etag = self._makeOne() self.assertEqual(etag.__nonzero__(), False) def test___contains__something(self): etag = self._makeOne() assert 'anything' not in etag def test___str__(self): etag = self._makeOne() self.assertEqual(str(etag), '') class ParseTests(unittest.TestCase): def test_parse_None(self): et = ETagMatcher.parse(None) self.assertEqual(et.etags, []) def test_parse_anyetag(self): # these tests smell bad, are they useful? et = ETagMatcher.parse('*') self.assertEqual(et.__dict__, {}) self.assertEqual(et.__repr__(), '') def test_parse_one(self): et = ETagMatcher.parse('"ONE"') self.assertEqual(et.etags, ['ONE']) def test_parse_invalid(self): for tag in ['one', 'one, two', '"one two']: et = ETagMatcher.parse(tag) self.assertEqual(et.etags, [tag]) et = ETagMatcher.parse('"foo" and w/"weak"', strong=False) self.assertEqual(et.etags, ['foo']) def test_parse_commasep(self): et = ETagMatcher.parse('"ONE", "TWO"') self.assertEqual(et.etags, ['ONE', 'TWO']) def test_parse_commasep_w_weak(self): et = ETagMatcher.parse('"ONE", W/"TWO"') self.assertEqual(et.etags, ['ONE']) et = ETagMatcher.parse('"ONE", W/"TWO"', strong=False) self.assertEqual(et.etags, ['ONE', 'TWO']) def test_parse_quoted(self): et = ETagMatcher.parse('"ONE"') self.assertEqual(et.etags, ['ONE']) def test_parse_quoted_two(self): et = ETagMatcher.parse('"ONE", "TWO"') self.assertEqual(et.etags, ['ONE', 'TWO']) def test_parse_quoted_two_weak(self): et = ETagMatcher.parse('"ONE", W/"TWO"') self.assertEqual(et.etags, ['ONE']) et = ETagMatcher.parse('"ONE", W/"TWO"', strong=False) self.assertEqual(et.etags, ['ONE', 'TWO']) class IfRangeTests(unittest.TestCase): def test___repr__(self): self.assertEqual(repr(IfRange(None)), 'IfRange(None)') def test___repr__etag(self): self.assertEqual(repr(IfRange('ETAG')), "IfRange('ETAG')") def test___repr__date(self): ir = IfRange.parse('Fri, 09 Nov 2001 01:08:47 GMT') self.assertEqual( repr(ir), 'IfRangeDate(datetime.datetime(2001, 11, 9, 1, 8, 47, tzinfo=UTC))' ) WebOb-1.3.1/.gitignore0000644000175000017500000000034112250510170015265 0ustar chrismchrism00000000000000WebOb.egg-info *.pyc *.pyo *.swp *~ TEST* testenv docs/_build/* dist/ build/ .coverage *.egg glob:_coverage Session.vim *_coverage/** *,cover *_fixt.py *$py.class .tox/ coverage.xml nosetests.xml env*/ _website/ __pycache__/