diff options
Diffstat (limited to 'docs/test_response.txt')
-rw-r--r-- | docs/test_response.txt | 446 |
1 files changed, 446 insertions, 0 deletions
diff --git a/docs/test_response.txt b/docs/test_response.txt new file mode 100644 index 0000000..9a5b47d --- /dev/null +++ b/docs/test_response.txt @@ -0,0 +1,446 @@ +This 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; expires="... GMT"; Max-Age=10000; Path=/' + >>> res.headers.getall('set-cookie') + ['test=value; Path=/', 'test2=value2; expires="... GMT"; Max-Age=10000; Path=/'] + >>> res.unset_cookie('test') + >>> res.headers.getall('set-cookie') + ['test2=value2; expires="... GMT"; Max-Age=10000; Path=/'] + >>> res.set_cookie('test2', 'value2-add') + >>> res.headers.getall('set-cookie') + ['test2=value2; expires="... GMT"; Max-Age=10000; Path=/', '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 + <CacheControl ''> + >>> 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 + <CacheControl 'max-age=10'> + >>> 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 + <CacheControl ''> + >>> 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']) + (<ContentRange bytes 0-99/1000>, '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) + <Response ... 304 Not Modified> + >>> res.etag = 'other-tag' + >>> req.get_response(res) + <Response ... 200 OK> + >>> 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) + <Response ... 200 OK> + >>> res.last_modified = None + >>> req.get_response(res) + <Response ... 200 OK> + +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 + <Range ranges=(1, 5)> + >>> str(req.range) + 'bytes=1-4' + >>> result = req.get_response(res) + >>> result.body + '1234' + >>> result.content_range.stop + 5 + >>> result.content_range + <ContentRange bytes 1-4/10> + >>> 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 + <BLANKLINE> + 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 + <BLANKLINE> + 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 + <ContentRange bytes 5-9/10> + + +Ranges requesting x last bytes are supported too: + + >>> req.range = 'bytes=-1' + >>> req.range + <Range ranges=(-1, None)> + >>> result = req.get_response(res) + >>> result.body + '9' + >>> result.content_range + <ContentRange bytes 9-9/10> + >>> 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 + <ContentRange bytes */10> + >>> 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 + <Range ranges=(1, 5)> + >>> result = req.get_response(res) + >>> result.body + '1234' + >>> result.content_range + <ContentRange bytes 1-4/10> + >>> # And trying If-modified-since + >>> res.etag = 'foobar' + >>> req.if_range = 'foobar' + >>> req.if_range + <IfRange etag=foobar, date=*> + >>> result = req.get_response(res) + >>> result.content_range + <ContentRange bytes 1-4/10> + >>> 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 + <ContentRange bytes 1-4/10> + >>> 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 bytes */*> + >>> ContentRange.parse('bytes */10') + <ContentRange bytes */10> + >>> ContentRange.parse('bytes 5-9/10') + <ContentRange bytes 5-9/10> + >>> ContentRange.parse('bytes 5-10/*') + <ContentRange 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.exception.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()) + <Response at ... 405 Method Not Allowed> + +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 + Content-Length: 3 + <BLANKLINE> + foo + >>> print r2 + 200 OK + Content-Length: 7 + <BLANKLINE> + 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. |