Merge remote-tracking branch 'upstream/master' into testing_refactor

Conflicts:
	tests/test_headers.py
This commit is contained in:
Jamie Painter
2013-02-06 09:28:37 -05:00
8 changed files with 178 additions and 38 deletions

View File

@@ -55,7 +55,7 @@ class API(object):
"""
req = Request(env)
resp = Response()
resp = Response(self._media_type)
responder, params = self._get_responder(req.path, req.method)
@@ -75,9 +75,12 @@ class API(object):
#
use_body = not should_ignore_body(resp.status, req.method)
if use_body:
set_content_length(resp)
content_length = set_content_length(resp)
else:
content_length = 0
start_response(resp.status, resp._wsgi_headers(self._media_type))
set_content_type = (content_length != 0)
start_response(resp.status, resp._wsgi_headers(set_content_type))
# Return an iterable for the body, per the WSGI spec
if use_body:

View File

@@ -36,8 +36,8 @@ IGNORE_BODY_STATUS_CODES = set([
'204 No Content',
'304 Not Modified',
'100 Continue',
'101 Switching Protocols',
'102 Processing'])
'101 Switching Protocols'
])
def should_ignore_body(status, method):
@@ -67,21 +67,23 @@ def set_content_length(resp):
resp: The response object on which to set the content length.
"""
content_length = 0
if resp.body is not None:
# Since body is assumed to be a byte string (str in Python 2, bytes in
# Python 3), figure out the length using standard functions.
resp.set_header('Content-Length', str(len(resp.body)))
content_length = len(resp.body)
elif resp.stream is not None:
if resp.stream_len is not None:
# Total stream length is known in advance (e.g., streaming a file)
resp.set_header('Content-Length', str(resp.stream_len))
content_length = resp.stream_len
else:
# Stream given, but length is unknown (dynamically-generated body)
pass
else:
# No body given
resp.set_header('Content-Length', '0')
# ...do not set the header.
return -1
resp.set_header('Content-Length', str(content_length))
return content_length
def prepare_wsgi_content(resp):

View File

@@ -192,6 +192,23 @@ class HTTPUnsupportedMediaType(HTTPError):
description, **kwargs)
class HTTPUpgradeRequired(HTTPError):
"""426 Upgrade Required
Sets title to "Upgrade Required".
Args:
description: Human-friendly description of the error, along with a
helpful suggestion or two.
The remaining (optional) args are the same as for HTTPError.
"""
def __init__(self, description, **kwargs):
HTTPError.__init__(self, HTTP_426, 'Upgrade Required',
description, **kwargs)
class HTTPInternalServerError(HTTPError):
"""500 Internal Server Error
@@ -218,8 +235,30 @@ class HTTPServiceUnavailable(HTTPError):
"""503 Service Unavailable
Args:
Same as for HTTPError, exept status is set for you.
title: Human-friendly error title. Set to None if you wish Falcon
to return an empty response body (all remaining args will
be ignored except for headers.) Do this only when you don't
wish to disclose sensitive information about why a request was
refused, or if the status and headers are self-descriptive.
description: Human-friendly description of the error, along with a
helpful suggestion or two (default None).
retry_after: Value for the Retry-After header. If a date object, will
serialize as an HTTP date. Otherwise, a non-negative int is
expected, representing the number of seconds to wait. See
also: http://goo.gl/DIrWr
headers: A dictionary of extra headers to return in the
response to the client (default None).
href: A URL someone can visit to find out more information
(default None).
href_rel: If href is given, use this value for the rel
attribute (default 'doc').
href_text: If href is given, use this as the friendly
title/description for the link (defaults to "API documentation
for this error").
code: An internal code that customers can reference in their
support request or to help them when searching for knowledge
base articles related to this error.
"""
def __init__(self, title, description, **kwargs):
def __init__(self, title, description, retry_after, **kwargs):
HTTPError.__init__(self, HTTP_503, title, description, **kwargs)

View File

@@ -20,11 +20,40 @@ CONTENT_TYPE_NAMES = set(['Content-Type', 'content-type', 'CONTENT-TYPE'])
class Response(object):
"""Represents an HTTP response to a client request"""
"""Represents an HTTP response to a client request
__slots__ = ('status', '_headers', 'body', 'data', 'stream', 'stream_len')
def __init__(self):
Attributes:
status: HTTP status code, such as "200 OK" (see also falcon.HTTP_*)
body: String representing response content. If Unicode, Falcon will
encode as UTF-8 in the response. If data is already a byte string,
use the data attribute instead (it's faster).
data: Byte string representing response content.
stream: Iterable stream-like object, representing response content.
stream_len: Expected length of stream (e.g., file size).
content_type: Value for the Content-Type header
etag: Value for the ETag header
cache_control: An array of cache directives (see http://goo.gl/fILS5
and http://goo.gl/sM9Xx for a good description.) The array will be
joined with ', ' to produce the value for the Cache-Control
header.
"""
__slots__ = (
'body',
'cache_control',
'content_type',
'data',
'etag',
'_headers',
'status',
'stream',
'stream_len'
)
def __init__(self, default_media_type):
"""Initialize response attributes to default values
Args:
@@ -33,13 +62,17 @@ class Response(object):
"""
self.status = '200 OK'
self._headers = []
self._headers = [('Content-Type', default_media_type)]
self.body = None
self.data = None
self.stream = None
self.stream_len = None
self.content_type = None
self.etag = None
self.cache_control = None
def set_header(self, name, value):
"""Set a header for this response to a given value.
@@ -75,15 +108,24 @@ class Response(object):
self._headers.extend(headers.items())
def _wsgi_headers(self, default_media_type):
"""Convert headers into the format expected by WSGI servers"""
def _wsgi_headers(self, set_content_type):
"""Convert headers into the format expected by WSGI servers
if (self.body is not None) or (self.stream is not None):
headers = self._headers
for name, value in headers:
if name in CONTENT_TYPE_NAMES:
break
else:
self._headers.append(('Content-Type', default_media_type))
WARNING: Only call once! Not idempotent.
return self._headers
"""
headers = self._headers
if not set_content_type:
del headers[0]
elif self.content_type is not None:
headers.append(('Content-Type', self.content_type))
if self.etag is not None:
headers.append(('ETag', self.etag))
if self.cache_control is not None:
headers.append(('Cache-Control', ', '.join(self.cache_control)))
return headers

View File

@@ -18,7 +18,6 @@ limitations under the License.
HTTP_100 = '100 Continue'
HTTP_101 = '101 Switching Protocols'
HTTP_102 = '102 Processing'
HTTP_200 = '200 OK'
HTTP_201 = '201 Created'
@@ -51,11 +50,12 @@ HTTP_409 = '409 Conflict'
HTTP_410 = '410 Gone'
HTTP_411 = '411 Length Required'
HTTP_412 = '412 Precondition Failed'
HTTP_413 = '413 Request Entity Too Large'
HTTP_414 = '414 Request-URI Too Large'
HTTP_413 = '413 Payload Too Large'
HTTP_414 = '414 URI Too Long'
HTTP_415 = '415 Unsupported Media Type'
HTTP_416 = '416 Requested range not satisfiable'
HTTP_416 = '416 Range Not Satisfiable'
HTTP_417 = '417 Expectation Failed'
HTTP_426 = '426 Upgrade Required'
HTTP_500 = '500 Internal Server Error'
HTTP_501 = '501 Not Implemented'

View File

@@ -4,7 +4,6 @@ from wsgiref.simple_server import make_server
def application(environ, start_response):
wsgi_errors = environ['wsgi.errors']
pdb.set_trace()
start_response("200 OK", [
('Content-Type', 'text/plain')])

View File

@@ -1,7 +1,7 @@
from testtools.matchers import Contains, Not
import falcon
import falcon.testing as testing
import falcon
class StatusTestResource:
@@ -32,6 +32,34 @@ class DefaultContentTypeResource:
resp.body = self.body
class HeaderHelpersResource:
def on_get(self, req, resp):
resp.body = "{}"
resp.content_type = 'x-falcon/peregrine'
resp.cache_control = [
'public', 'private', 'no-cache', 'no-store', 'must-revalidate',
'proxy-revalidate', 'max-age=3600', 's-maxage=60', 'no-transform'
]
resp.etag = 'fa0d1a60ef6616bb28038515c8ea4cb2'
# resp.set_last_modified() # http://goo.gl/M9Fs9
# resp.set_retry_after() # http://goo.gl/DIrWr
# resp.set_vary() # http://goo.gl/wyI7d
# # Relative URI's are OK per http://goo.gl/DbVqR
# resp.set_location('/things/87')
# bytes 0-499/10240
# resp.set_content_range(0, 499, 10 * 1024)
def on_head(self, req, resp):
# Alias of set_media_type
resp.content_type = 'x-falcon/peregrine'
resp.cache_control = ['no-store']
class TestHeaders(testing.TestSuite):
def prepare(self):
@@ -78,8 +106,8 @@ class TestHeaders(testing.TestSuite):
host = self.resource.req.get_header('host')
self.assertEquals(host, 'localhost:8000')
def test_no_body_on_1xx(self):
self.resource = StatusTestResource(falcon.HTTP_102)
def test_no_body_on_100(self):
self.resource = StatusTestResource(falcon.HTTP_100)
self.api.add_route('/1xx', self.resource)
body = self._simulate_request('/1xx')
@@ -154,6 +182,33 @@ class TestHeaders(testing.TestSuite):
content_type = 'application/atom+xml'
self.assertIn(('Content-Type', content_type), self.srmock.headers)
def test_response_header_helpers_on_get(self):
self.resource = HeaderHelpersResource()
self.api.add_route(self.test_route, self.resource)
self._simulate_request(self.test_route)
content_type = 'x-falcon/peregrine'
self.assertIn(('Content-Type', content_type), self.srmock.headers)
cache_control = ('public, private, no-cache, no-store, '
'must-revalidate, proxy-revalidate, max-age=3600, '
's-maxage=60, no-transform')
self.assertIn(('Cache-Control', cache_control), self.srmock.headers)
etag = 'fa0d1a60ef6616bb28038515c8ea4cb2'
self.assertIn(('ETag', etag), self.srmock.headers)
def test_response_header_helpers_on_head(self):
self.resource = HeaderHelpersResource()
self.api.add_route(self.test_route, self.resource)
self._simulate_request(self.test_route, method="HEAD")
content_type = 'x-falcon/peregrine'
self.assertNotIn(('Content-Type', content_type), self.srmock.headers)
self.assertIn(('Cache-Control', 'no-store'), self.srmock.headers)
def test_no_content_type(self):
self.resource = DefaultContentTypeResource()
self.api.add_route(self.test_route, self.resource)

View File

@@ -12,9 +12,9 @@ class TestReqVars(testing.TestSuite):
}
self.req = Request(testing.create_environ(script='/test',
path='/hello',
query_string=qs,
headers=headers))
path='/hello',
query_string=qs,
headers=headers))
def test_reconstruct_url(self):
req = self.req