diff options
author | Chris McDonough <chrism@plope.com> | 2011-03-20 23:25:37 -0400 |
---|---|---|
committer | Chris McDonough <chrism@plope.com> | 2011-03-20 23:25:37 -0400 |
commit | d8176e7279e19ab3e618056555a05d710bb161d4 (patch) | |
tree | 43717841fd1326d22a4aaea2d1a17c1c0ce72fcb /docs | |
parent | 1c4912af221e89ecf117154e2708f58984287ad8 (diff) | |
parent | c3f021fbf2fd027b0778ca9dadc79ae7b2a5394b (diff) | |
download | webob-tests.pycon2011.tar.gz |
merge from headtests.pycon2011
Diffstat (limited to 'docs')
-rw-r--r-- | docs/doctests.py | 17 | ||||
-rw-r--r-- | docs/file-example.txt | 2 | ||||
-rw-r--r-- | docs/index.txt | 2 | ||||
-rw-r--r-- | docs/reference.txt | 2 | ||||
-rw-r--r-- | docs/test_dec.txt | 103 | ||||
-rw-r--r-- | docs/test_request.txt | 553 | ||||
-rw-r--r-- | docs/test_response.txt | 446 |
7 files changed, 1122 insertions, 3 deletions
diff --git a/docs/doctests.py b/docs/doctests.py new file mode 100644 index 0000000..9aecb31 --- /dev/null +++ b/docs/doctests.py @@ -0,0 +1,17 @@ +import 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') diff --git a/docs/file-example.txt b/docs/file-example.txt index 601e947..16d4d19 100644 --- a/docs/file-example.txt +++ b/docs/file-example.txt @@ -10,7 +10,7 @@ to a high-quality file serving application. >>> import webob, os >>> base_dir = os.path.dirname(os.path.dirname(webob.__file__)) >>> doc_dir = os.path.join(base_dir, 'docs') - >>> from dtopt import ELLIPSIS + >>> from doctest import ELLIPSIS First we'll setup a really simple shim around our application, which we can use as we improve our application: diff --git a/docs/index.txt b/docs/index.txt index bad7be6..2e5c898 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -19,7 +19,7 @@ WebOb .. comment: - >>> from dtopt import ELLIPSIS + >>> from doctest import ELLIPSIS Status & License diff --git a/docs/reference.txt b/docs/reference.txt index 188693b..b12ec40 100644 --- a/docs/reference.txt +++ b/docs/reference.txt @@ -5,7 +5,7 @@ WebOb Reference .. comment: - >>> from dtopt import ELLIPSIS + >>> from doctest import ELLIPSIS Introduction ============ diff --git a/docs/test_dec.txt b/docs/test_dec.txt new file mode 100644 index 0000000..df0461c --- /dev/null +++ b/docs/test_dec.txt @@ -0,0 +1,103 @@ +A 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 + <BLANKLINE> + 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 + <BLANKLINE> + 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 + <BLANKLINE> + 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 + <BLANKLINE> + 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 + <BLANKLINE> + hey + >>> print Request.blank('/500').get_response(app) + 200 OK + Content-Type: text/html; charset=UTF-8 + Content-Length: 6 + <BLANKLINE> + error! + >>> print Request.blank('/404').get_response(app) + 200 OK + Content-Type: text/html; charset=UTF-8 + Content-Length: 7 + <BLANKLINE> + nothing diff --git a/docs/test_request.txt b/docs/test_request.txt new file mode 100644 index 0000000..66d26dd --- /dev/null +++ b/docs/test_request.txt @@ -0,0 +1,553 @@ +This demonstrates how the Request object works, and tests it. + +To test for absense of PendingDeprecationWarning's we should reset +default warning filters + + >>> import warnings + >>> warnings.resetwarnings() + + +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 + <Request at ... GET http://localhost/> + >>> 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)) + >>> vars = req.str_POST + >>> vars + MultiDict([('var1', 'value1'), ('var2', 'value2'), ('rep', '1'), ('rep', '2')]) + >>> vars is req.POST + False + >>> req.POST + UnicodeMultiDict([('var1', u'value1'), ('var2', u'value2'), ('rep', u'1'), ('rep', u'2')]) + >>> req.decode_param_names + False + +Note that the variables are there for GET requests and non-form requests, +but they are empty and read-only: + + >>> req = Request.blank('/') + >>> req.str_POST + <NoVars: Not a form request> + >>> req.str_POST.items() + [] + >>> req.str_POST['x'] = 'y' + Traceback (most recent call last): + ... + KeyError: 'Cannot add variables: Not a form request' + >>> req.method = 'POST' + >>> req.str_POST + MultiDict([]) + >>> req.content_type = 'text/xml' + >>> req.body_file = StringIO('<xml></xml>') + >>> req.str_POST + <NoVars: Not an HTML form submission (Content-Type: text/xml)> + >>> req.body + '<xml></xml>' + +You can also get access to the query string variables, of course: + + >>> req = Request.blank('/?a=b&d=e&d=f') + >>> req.str_GET + GET([('a', 'b'), ('d', 'e'), ('d', '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 + UnicodeMultiDict([('a', u'b'), ('d', u'e'), ('d', u'f'), ('x', u'y'), ('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.str_cookies.items()) + [('var1', 'value1'), ('var2', 'value2')] + >>> sorted(req.cookies.items()) + [('var1', u'value1'), ('var2', u'value2')] + >>> req.charset = 'utf8' + >>> sorted(req.cookies.items()) + [('var1', u'value1'), ('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 + <CacheControl ''> + >>> req.cache_control.no_cache = True + >>> req.cache_control.max_age = 0 + >>> req.cache_control + <CacheControl 'max-age=0, no-cache'> + +.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.5" + >>> req.accept # doctest: +ELLIPSIS + <MIMEAccept at ... Accept: text/*;q=0.3, text/html;q=0.7, text/html, text/html;q=0.4, */*;q=0.5> + >>> 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.5 + >>> '%0.1f' % req.accept.quality('text/html') + '0.3' + >>> req.accept.first_match(['text/plain', 'text/html', 'image/png']) + 'text/plain' + >>> '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 + <MIMEAccept at ... Accept: text/html, application/xml;q=0.7, text/*;q=0.5, */*;q=0.1> + >>> req.accept.best_match(['text/plain', 'application/xml']) + 'application/xml' + >>> req.accept.first_match(['application/xml', 'text/html']) + '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 + <IfRange etag=asdf, date=*> + >>> 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 + <IfRange etag=*, date=Sun, 01 Jan 2006 12:00:00 GMT> + >>> 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 + <Empty If-Range> + >>> req.if_range.match_response(res) + True + +Ranges work like so:: + + >>> req = Request.blank('/') + >>> req.range = (0, 100) + >>> req.range + <Range ranges=(0, 100)> + >>> 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 + <ContentRange bytes 0-99/1000> + >>> 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.body_file = data + >>> req.body_file_raw + <...IO... object at ...> + >>> len(req.body) + 10250 + >>> req.body_file + <open file ..., mode 'w+b' at ...> + >>> int(req.body_file.tell()) + 0 + >>> req.POST + UnicodeMultiDict([]) + >>> 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.str_GET + GET([('a', 'b')]) + >>> req.str_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 + ... + ... <html>Some text...</html> + ... --foobar-- + ... ''' + >>> req.str_POST + MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html'))]) + >>> print req.body.replace('\r', '') # doctest: +REPORT_UDIFF + --foobar + Content-Disposition: form-data; name="a" + <BLANKLINE> + b + --foobar + Content-Disposition: form-data; name="upload"; filename="test.html" + Content-type: text/html + <BLANKLINE> + <html>Some text...</html> + --foobar-- + >>> req.POST['c'] = 'd' + >>> req.str_POST + MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html')), ('c', 'd')]) + >>> req.body_file_raw + <FakeCGIBody at ... viewing MultiDict([('a'...d')])> + >>> sorted(req.str_POST.keys()) + ['a', 'c', 'upload'] + >>> print req.body.replace('\r', '') # doctestx: +REPORT_UDIFF + --foobar + Content-Disposition: form-data; name="a" + <BLANKLINE> + b + --foobar + Content-Disposition: form-data; name="upload"; filename="test.html" + Content-type: text/html + <BLANKLINE> + <html>Some text...</html> + --foobar + Content-Disposition: form-data; name="c" + <BLANKLINE> + 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 + ... + ... <html>Some text...</html> + ... --foobar-- + ... ''' + >>> req_.str_POST + MultiDict([('a', 'b'), ('upload', FieldStorage('upload', 'test.html'))]) + >>> print req_.body.replace('\r', '') # doctest: +REPORT_UDIFF + --foobar + Content-Disposition: form-data; name="a" + <BLANKLINE> + b + --foobar + Content-Disposition: form-data; name="upload"; filename="test.html" + Content-type: text/html + <BLANKLINE> + <html>Some text...</html> + --foobar-- + >>> req_.POST['c'] = 'd' + >>> req_.str_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', '<html>Some text...</html>\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.str_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(req) returns the request as HTTP request string + + >>> print req + PUT / HTTP/1.0 + Content-Length: 10 + Host: localhost:80 + <BLANKLINE> + xxxxxxxxxx + +req.as_string() does the same but also can take additional argument `skip_body` +skip_body=True excludes the body from the result + + >>> print req.as_string(skip_body=True) + PUT / HTTP/1.0 + Content-Length: 10 + Host: localhost:80 + + +skip_body=<int> excludes the body from the result if it's longer than that number + + >>> print req.as_string(skip_body=5) + PUT / HTTP/1.0 + Content-Length: 10 + Host: localhost:80 + <BLANKLINE> + <body skipped (len=10)> + +but not if it's shorter + + >>> print req.as_string(skip_body=100) + PUT / HTTP/1.0 + Content-Length: 10 + Host: localhost:80 + <BLANKLINE> + xxxxxxxxxx + 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. |