summaryrefslogtreecommitdiff
path: root/docs
diff options
context:
space:
mode:
Diffstat (limited to 'docs')
-rw-r--r--docs/doctests.py17
-rw-r--r--docs/file-example.txt2
-rw-r--r--docs/index.txt2
-rw-r--r--docs/reference.txt2
-rw-r--r--docs/test_dec.txt103
-rw-r--r--docs/test_request.txt553
-rw-r--r--docs/test_response.txt446
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.