Merge branch 'master' of git://github.com/racker/falcon into pythonic_rand_chars

Conflicts:
	test/helpers.py
This commit is contained in:
Alejandro Cabrera
2013-01-09 16:28:57 -05:00
22 changed files with 869 additions and 145 deletions

View File

@@ -1,5 +1,27 @@
* Keep-Alive is intentially disabled for HTTP/1.0 clients to mitigate problems with proxies. See also http://tools.ietf.org/html/rfc2616#section-19.6.2
### Assumptions ###
In order to stay lean and fast, Falcon makes several assumptions.
First, Falcon assumes that request handlers will (for the most part) do the right thing. In other words, Falcon doesn't try very hard to protect handler code from itself.
This requires some discipline on the part of the developer.
1. Request handlers will set response variables to sane values.
1. The application won't add extra junk to req and resp dicts (use the ctx instead)
1. Request handlers are well-tested with high code coverage. It's not Falcon's job to babysit your code once it leaves the nest.
1. ...
### Misc. ###
* Falcon probably isn't thread-safe; don't try it. Run multiple worker processes, each with a non-blocking I/O loop instead.
* Falcon doesn't officially support Python 3; it's on our TODO list.
* Falcon is based on byte strings, and does no conversions to UTF-16 (for example). If your app needs to use wide strings, you'll need to do the conversion manually. However, we recommend just keeping everything UTF-8 to avoid writing extra code and spinning CPU cycles.
* resp.set_header assumes both params are strings. App may crash otherwise. Falcon trusts the caller. You *are* testing all your code paths, aren't you?
* If you need the protocol (http vs https) to construct hrefs in your responses (hypermedia is good, trust me), you can get it from req.scheme
* URI template and query string field names must include only ASCII a-z, A-Z, and the underscore '_' character. Try it; you'll like it. This simplifies parsing and helps speed things up a bit.
* Query params must have a value. In other words, 'foo' or 'foo=' will result in the parameter being ignored.
* If the WSGI server passes an empty path, Falcon will force it to '/', so you don't have to test for the empty string in your app.
* If you are hosting multiple apps with a single WSGI server, the SCRIPT_NAME variable can read from req.app
* If you have several headers to set, consider using set_headers to avoid function call overhead
* Don't set content-length. It will only be overridden.
* The order in which header fields are sent in the response is undefined. Headers are not grouped according to the recommendation in [RFC 2616](http://tools.ietf.org/html/rfc2616#section-4.2) in order to generate responses as quickly as possible.

View File

@@ -11,7 +11,7 @@ Falcon is a fast, light-weight framework for building cloud APIs. It tries to do
>
> *- Antoine de Saint-Exupéry*
### Design ###
### Design Goals ###
**Light-weight.** Only the essentials are included, with few dependencies. We work to keep the code lean-n-mean, making Falcon easier to test, optimize, and deploy.
@@ -19,18 +19,9 @@ Falcon is a fast, light-weight framework for building cloud APIs. It tries to do
**Cloud-friendly.** Falcon uses the web-friendly Python language, and speaks WSGI, so you can deploy it on your favorite stack. The framework is designed from the ground up to embrace HTTP, not work against it. Plus, diagnostics are built right in to make it easier to track down sneaky bugs and frustrating performance problems.
### Assumptions ###
### Contributing ###
(Work in progress.)
Pull requests are welcome. Just make sure to include tests and follow PEP 8 and your commit messages are formatted using [AngularJS conventions][ajs] (one-liners are OK for now but body and footer may be required as the project matures).
In order to stay lean and fast, Falcon makes several assumptions.
First of all, Falcon assumes that request handlers will (for the most part) do the right thing. In other words, Falcon doesn't try very hard to protect handler code from itself.
This requires some discipline on the part of the developer.
1. Request handlers will set response variables to sane values. This includes setting *status* to a valid HTTP status code and string (just use the provided constants), setting *headers* to a collection of tuples, and setting *body* (if not desired to be empty) to either a string or an iterable.
1. The application won't add extra junk to req and resp dicts (use the ctx instead)
1. Request handlers are well-tested with high code coverage. It's not Falcon's job to babysit your code once it leaves the nest.
1. ...
[ajs]: http://goo.gl/QpbS7

View File

@@ -1,6 +1,7 @@
"""A fast micro-framework for building cloud APIs."""
version_tuple = (0, 0, 1, '-dev')
def get_version_string():
if isinstance(version_tuple[-1], basestring):
return '.'.join(map(str, version_tuple[:-1])) + version_tuple[-1]

View File

@@ -1,17 +1,28 @@
from falcon.request import Request
from falcon.response import Response
from falcon.default_request_handlers import *
from falcon import responders
from falcon.status_codes import *
from falcon.api_helpers import *
HTTP_METHODS = (
'CONNECT',
'DELETE',
'GET',
'HEAD',
'OPTIONS',
'POST',
'PUT',
'TRACE'
)
# TODO: __slots__
# TODO: log exceptions, trace execution, etc.
class Api:
"""Provides routing and such for building a web service application"""
__slots__ = ('routes')
def __init__(self):
self.routes = {}
self.routes = []
def __call__(self, env, start_response):
"""WSGI protocol handler"""
@@ -25,22 +36,28 @@ class Api:
req = Request(env)
resp = Response()
# PERF: Use try...except blocks when the key usually exists
try:
# TODO: Figure out a way to use codegen to make a state machine,
# may have to in order to support URI templates.
handler = self.routes[req.path]
except KeyError:
handler = path_not_found_handler
path = req.path
for path_template, method_map in self.routes:
m = path_template.match(path)
if m:
req._params.update(m.groupdict())
try:
handler = method_map[req.method]
except KeyError:
handler = responders.bad_request
else:
break
else:
handler = responders.path_not_found
handler(ctx, req, resp)
#
# Set status and headers
#
use_body = not self._should_ignore_body(resp.status)
use_body = not should_ignore_body(resp.status)
if use_body:
self._set_content_length(env, req, resp)
set_content_length(env, req, resp)
start_response(resp.status, resp._wsgi_headers())
@@ -51,27 +68,11 @@ class Api:
# Ignore body based on status code
return []
def add_route(self, uri_template, handler):
self.routes[uri_template] = handler
pass
if not uri_template:
uri_template = '/'
# -------------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------------
def _should_ignore_body(self, status):
return (status.startswith('204') or
status.startswith('1') or
status.startswith('304'))
def _set_content_length(self, env, req, resp):
# Set Content-Length when given a fully-buffered body or stream length
if resp.body is not None:
resp.set_header('Content-Length', str(len(resp.body)))
elif resp.stream_len is not None:
resp.set_header('Content-Length', resp.stream_len)
else:
resp.set_header('Content-Length', 0)
path_template = compile_uri_template(uri_template)
method_map = create_http_method_map(handler)
self.routes.append((path_template, method_map))

107
falcon/api.py.orig Normal file
View File

@@ -0,0 +1,107 @@
import re
from falcon.request import Request
from falcon.response import Response
from falcon.default_request_handlers import *
from falcon.status_codes import *
# TODO: __slots__
# TODO: log exceptions, trace execution, etc.
class Api:
"""Provides routing and such for building a web service application"""
def __init__(self):
self.routes = []
def __call__(self, env, start_response):
"""WSGI protocol handler"""
# PERF: Use literal constructor for dicts
ctx = {}
# TODO
# ctx.update(global_ctx_for_route)
req = Request(env)
resp = Response()
path = req.path
for path_template, handler in self.routes:
m = path_template.match(path)
if m:
req._params.update(m.groupdict())
break
else:
handler = path_not_found_handler
handler(ctx, req, resp)
#
# Set status and headers
#
use_body = not self._should_ignore_body(resp.status)
if use_body:
self._set_content_length(env, req, resp)
start_response(resp.status, resp._wsgi_headers())
# Return an iterable for the body, per the WSGI spec
if use_body:
return [resp.body] if resp.body is not None else []
# Ignore body based on status code
return []
def add_route(self, uri_template, handler):
if not hasattr(handler, '__call__'):
raise TypeError('handler is not callable')
if not uri_template:
uri_template = '/'
path_template = self._compile_uri_template(uri_template)
self.routes.append((path_template, handler))
# -------------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------------
def _should_ignore_body(self, status):
return (status.startswith('204') or
status.startswith('1') or
status.startswith('304'))
def _set_content_length(self, env, req, resp):
# Set Content-Length when given a fully-buffered body or stream length
if resp.body is not None:
resp.set_header('Content-Length', str(len(resp.body)))
elif resp.stream_len is not None:
resp.set_header('Content-Length', resp.stream_len)
else:
resp.set_header('Content-Length', 0)
def _compile_uri_template(self, template):
"""Compile the given URI template string into path and query string
regex-based templates.
See also: http://tools.ietf.org/html/rfc6570
"""
if not isinstance(template, str):
raise TypeError('uri_template is not a byte string')
# Convert Level 1 var patterns to equivalent named regex groups
pattern = re.sub(r'{([a-zA-Z][a-zA-Z_]*)}', r'(?P<\1>[^/]+)', template)
pattern = r'\A' + pattern + r'\Z'
return re.compile(pattern, re.IGNORECASE)
<<<<<<< Updated upstream
=======
def _get_handler_http_methods(self, handler):
pass
>>>>>>> Stashed changes

70
falcon/api_helpers.py Normal file
View File

@@ -0,0 +1,70 @@
import re
from falcon import responders
HTTP_METHODS = (
'CONNECT',
'DELETE',
'GET',
'HEAD',
'OPTIONS',
'POST',
'PUT',
'TRACE'
)
def should_ignore_body(status):
return (status.startswith('204') or
status.startswith('1') or
status.startswith('304'))
def set_content_length(env, req, resp):
# Set Content-Length when given a fully-buffered body or stream length
if resp.body is not None:
resp.set_header('Content-Length', str(len(resp.body)))
elif resp.stream_len is not None:
resp.set_header('Content-Length', resp.stream_len)
else:
resp.set_header('Content-Length', 0)
def compile_uri_template(template):
"""Compile the given URI template string into path and query string
regex-based templates.
See also: http://tools.ietf.org/html/rfc6570
"""
if not isinstance(template, str):
raise TypeError('uri_template is not a byte string')
# Convert Level 1 var patterns to equivalent named regex groups
pattern = re.sub(r'{([a-zA-Z][a-zA-Z_]*)}', r'(?P<\1>[^/]+)', template)
pattern = r'\A' + pattern + r'\Z'
return re.compile(pattern, re.IGNORECASE)
def create_http_method_map(handler):
method_map = {}
for method in HTTP_METHODS:
try:
func = getattr(handler, 'on_' + method.lower())
except AttributeError:
# Handler does not implement this method
pass
else:
# Usually expect a method, but any callable will do
if hasattr(func, '__call__'):
method_map[method] = func
# Attach a handler for unsupported HTTP methods
allowed_methods = method_map.keys()
func = responders.create_method_not_allowed(allowed_methods)
for method in HTTP_METHODS:
if method not in allowed_methods:
method_map[method] = func
return method_map

View File

@@ -1,4 +0,0 @@
from status_codes import *
def path_not_found_handler(ctx, req, resp):
resp.status = HTTP_404

View File

@@ -1,37 +1,57 @@
from falcon.request_helpers import *
class Request:
__slots__ = ('path', 'headers')
__slots__ = (
'app',
'body',
'_headers',
'method',
'_params',
'path',
'protocol',
'query_string'
)
def __init__(self, env):
self.path = env['PATH_INFO']
self.headers = headers = {}
self.app = env['SCRIPT_NAME']
self.body = env['wsgi.input']
self.method = env['REQUEST_METHOD']
self.path = env['PATH_INFO'] or '/'
self.protocol = env['wsgi.url_scheme']
self.query_string = query_string = env['QUERY_STRING']
self._params = parse_query_string(query_string)
self._headers = parse_headers(env)
# Extract HTTP headers
for key in env:
if key.startswith('HTTP_'):
headers[key[5:]] = env[key]
if 'HOST' not in headers:
host = env['SERVER_NAME']
port = env['SERVER_PORT']
if port != '80':
host = ''.join([host, ':', port])
headers['HOST'] = host
def get_header(self, name, default=None):
def try_get_header(self, name, default=None):
"""Return a header value as a string
name -- Header name, case-insensitive
name -- Header name, case-insensitive (e.g., 'Content-Type')
default -- Value to return in case the header is not found
"""
headers = self.headers
# Optimize for the header existing in most cases
# Use try..except to optimize for the header existing in most cases
try:
return headers[name.upper()]
except KeyError as e:
# Don't take the time to cache beforehand, using HTTP naming.
# This will be faster, assuming that most headers are looked
# up only once, and not all headers will be requested.
return self._headers[name.upper().replace('-', '_')]
except KeyError:
return default
def try_get_param(self, name, default=None):
"""Return a URI parameter value as a string
name -- Parameter name as specified in the route template. Note that
names are case-sensitive (e.g., 'Id' != 'id').
default -- Value to return in case the header is not found
"""
# PERF: Use if..in since it is a good all-around performer; we don't
# know how likely params are to be specified by clients.
if name in self._params:
return self._params[name]
return default

44
falcon/request_helpers.py Normal file
View File

@@ -0,0 +1,44 @@
import re
QS_PATTERN = re.compile(r'([a-zA-Z_]+)=([^&]+)')
def parse_query_string(query_string):
# Parse query string
# PERF: use for loop in lieu of the dict constructor
params = {}
for k, v in QS_PATTERN.findall(query_string):
if ',' in v:
v = v.split(',')
params[k] = v
return params
def parse_headers(env):
# Parse HTTP_*
headers = {}
for key in env:
if key.startswith('HTTP_'):
headers[key[5:]] = env[key]
# Per the WSGI spec, Content-Type is not under HTTP_*
if 'CONTENT_TYPE' in env:
headers['CONTENT_TYPE'] = env['CONTENT_TYPE']
# Per the WSGI spec, Content-Length is not under HTTP_*
if 'CONTENT_LENGTH' in env:
headers['CONTENT_LENGTH'] = env['CONTENT_LENGTH']
# Fallback to SERVER_* vars if the Host header isn't specified
if 'HOST' not in headers:
host = env['SERVER_NAME']
port = env['SERVER_PORT']
if port != '80':
host = ''.join([host, ':', port])
headers['HOST'] = host
return headers

15
falcon/responders.py Normal file
View File

@@ -0,0 +1,15 @@
from falcon.status_codes import *
def path_not_found(ctx, req, resp):
resp.status = HTTP_404
def bad_request(ctx, req, resp):
resp.status = HTTP_400
def create_method_not_allowed(allowed_methods):
def method_not_allowed(ctx, req, resp):
resp.status = HTTP_405
resp.set_header('Allow', ', '.join(allowed_methods))
return method_not_allowed

View File

@@ -14,12 +14,100 @@ HTTP_206 = '206 Partial Content'
HTTP_226 = '226 IM Used'
# TODO: 3xx
HTTP_300 = '300 Multiple Choices'
HTTP_301 = '301 Moved Permanently'
HTTP_302 = '302 Found'
HTTP_303 = '303 See Other'
HTTP_304 = '304 Not Modified'
HTTP_305 = '305 Use Proxy'
HTTP_307 = '307 Temporary Redirect'
# TODO: 4xx
HTTP_400 = '400 Bad Request'
HTTP_401 = '401 Unauthorized' # <-- Really means "unauthenticated"
HTTP_402 = '402 Payment Required'
HTTP_403 = '403 Forbidden' # <-- Really means "unauthorized"
HTTP_404 = '404 Not Found'
HTTP_405 = '405 Method Not Allowed'
HTTP_406 = '406 Not Acceptable'
HTTP_407 = '407 Proxy Authentication Required'
HTTP_408 = '408 Request Time-out'
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_415 = '415 Unsupported Media Type'
HTTP_416 = '416 Requested range not satisfiable'
HTTP_417 = '417 Expectation Failed'
# TODO: 5xx
HTTP_500 = '500 Internal Server Error'
HTTP_501 = '501 Not Implemented'
HTTP_502 = '502 Bad Gateway'
HTTP_503 = '503 Service Unavailable'
HTTP_504 = '504 Gateway Time-out'
HTTP_505 = '505 HTTP Version not supported'
# TODO: 7xx
# 70X - Inexcusable
HTTP_701 = '701 Meh'
HTTP_702 = '702 Emacs'
HTTP_703 = '703 Explosion'
# 71X - Novelty Implementations
HTTP_710 = '710 PHP'
HTTP_711 = '711 Convenience Store'
HTTP_712 = '712 NoSQL'
HTTP_719 = '719 I am not a teapot'
# 72X - Edge Cases
HTTP_720 = '720 Unpossible'
HTTP_721 = '721 Known Unknowns'
HTTP_722 = '722 Unknown Unknowns'
HTTP_723 = '723 Tricky'
HTTP_724 = '724 This line should be unreachable'
HTTP_725 = '725 It works on my machine'
HTTP_726 = "726 It's a feature, not a bug"
HTTP_727 = '727 32 bits in plenty'
# 74X - Meme Driven
HTTP_740 = '740 Computer says no'
HTTP_741 = '741 Compiling'
HTTP_742 = '742 A kitten dies'
HTTP_743 = '743 I thought I knew regular expressions'
HTTP_744 = '744 Y U NO write integration tests?'
HTTP_745 = ("745 I don't always test my code, but when I do"
"I do it in production")
HTTP_748 = '748 Confounded by Ponies'
HTTP_749 = '749 Reserved for Chuck Norris'
# 75X - Syntax Errors
HTTP_750 = "750 Didn't bother to compile it"
HTTP_753 = '753 Syntax Error'
HTTP_754 = '754 Too many semi-colons'
HTTP_755 = '755 Not enough semi-colons'
HTTP_759 = '759 Unexpected T_PAAMAYIM_NEKUDOTAYIM'
# 77X - Predictable Problems
HTTP_771 = '771 Cached for too long'
HTTP_772 = '772 Not cached long enough'
HTTP_773 = '773 Not cached at all'
HTTP_774 = '774 Why was this cached?'
HTTP_776 = '776 Error on the Exception'
HTTP_777 = '777 Coincidence'
HTTP_778 = '778 Off By One Error'
HTTP_779 = '779 Off By Too Many To Count Error'
# 78X - Somebody Else's Problem
HTTP_780 = '780 Project owner not responding'
HTTP_781 = '781 Operations'
HTTP_782 = '782 QA'
HTTP_783 = '783 It was a customer request, honestly'
HTTP_784 = '784 Management, obviously'
HTTP_785 = '785 TPS Cover Sheet not attached'
HTTP_786 = '786 Try it now'
# 79X - Internet crashed
HTTP_791 = '791 The Internet shut down due to copyright restrictions.'
HTTP_792 = '792 Climate change driven catastrophic weather event'
HTTP_797 = '797 This is the last page of the Internet. Go back'
HTTP_799 = '799 End of the world'

View File

@@ -1,12 +1,13 @@
def application(environ, start_response):
start_response("200 OK", [
('Content-Type', 'text/plain')])
body = '\n{\n'
for key, value in environ.items():
if isinstance(value, basestring):
body += ' "{0}": "{1}",\n'.format(key, value)
#if isinstance(value, basestring):
body += ' "{0}": "{1}",\n'.format(key, value)
body += '}\n\n'

View File

@@ -1,10 +1,18 @@
import inspect
import random
from io import BytesIO
import testtools
import falcon
def rand_string(min, max):
int_gen = random.randint
string_length = int_gen(min, max)
return ''.join([chr(int_gen(9, 126))
for i in range(string_length)])
class StartResponseMock:
def __init__(self):
self._called = 0
@@ -20,6 +28,29 @@ class StartResponseMock:
def call_count(self):
return self._called
class RequestHandler:
sample_status = "200 OK"
sample_body = rand_string(0, 128 * 1024)
resp_headers = {
'Content-Type': 'text/plain; charset=utf-8',
'ETag': '10d4555ebeb53b30adf724ca198b32a2',
'X-Hello': 'OH HAI'
}
def __init__(self):
self.called = False
def on_get(self, ctx, req, resp):
self.called = True
self.ctx, self.req, self.resp = ctx, req, resp
resp.status = falcon.HTTP_200
resp.body = self.sample_body
resp.set_headers(self.resp_headers)
class TestSuite(testtools.TestCase):
def setUp(self):
@@ -33,34 +64,34 @@ class TestSuite(testtools.TestCase):
prepare()
def _simulate_request(self, path, **kwargs):
return self.api(create_environ(path=path, **kwargs),
self.srmock)
if not path:
path = '/'
def rand_string(min, max):
int_gen = random.randint
string_length = int_gen(min, max)
return ''.join([chr(int_gen(9, 126))
for i in range(string_length)])
return self.api(create_environ(path=path, **kwargs),
self.srmock)
def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
headers=None):
headers=None, script='', body='', method='GET'):
env = {
'SERVER_PROTOCOL': protocol,
'SERVER_SOFTWARE': 'gunicorn/0.17.0',
'SCRIPT_NAME': '',
'REQUEST_METHOD': 'GET',
'SCRIPT_NAME': script,
'REQUEST_METHOD': method,
'PATH_INFO': path,
'QUERY_STRING': query_string,
'HTTP_ACCEPT': '*/*',
'HTTP_USER_AGENT': 'curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5',
'HTTP_USER_AGENT': ('curl/7.24.0 (x86_64-apple-darwin12.0) '
'libcurl/7.24.0 OpenSSL/0.9.8r zlib/1.2.5'),
'REMOTE_PORT': '65133',
'RAW_URI': '/',
'REMOTE_ADDR': '127.0.0.1',
'wsgi_url_scheme': 'http',
'SERVER_NAME': 'localhost',
'SERVER_PORT': port,
'wsgi.url_scheme': 'http',
'wsgi.input': BytesIO(body)
}
if protocol != 'HTTP/1.0':
@@ -68,6 +99,13 @@ def create_environ(path='/', query_string='', protocol='HTTP/1.1', port='80',
if headers is not None:
for name, value in headers.iteritems():
env['HTTP_' + name.upper()] = value.strip()
name = name.upper().replace('-', '_')
if name == 'CONTENT_TYPE':
env[name] = value.strip()
elif name == 'CONTENT_LENGTH':
env[name] = value.strip()
else:
env['HTTP_' + name.upper()] = value.strip()
return env

View File

@@ -1,31 +1,8 @@
import testtools
import random
from testtools.matchers import Equals, MatchesRegex, Contains, Not
from testtools.matchers import Contains, Not
import falcon
import test.helpers as helpers
class RequestHandler:
sample_status = "200 OK"
sample_body = helpers.rand_string(0, 128 * 1024)
resp_headers = {
'Content-Type': 'text/plain; charset=utf-8',
'ETag': '10d4555ebeb53b30adf724ca198b32a2',
'X-Hello': 'OH HAI'
}
def __init__(self):
self.called = False
def __call__(self, ctx, req, resp):
self.called = True
self.ctx, self.req, self.resp = ctx, req, resp
resp.status = falcon.HTTP_200
resp.body = self.sample_body
resp.set_headers(self.resp_headers)
class RequestHandlerTestStatus:
sample_body = helpers.rand_string(0, 128 * 1024)
@@ -33,7 +10,7 @@ class RequestHandlerTestStatus:
def __init__(self, status):
self.status = status
def __call__(self, ctx, req, resp):
def on_get(self, ctx, req, resp):
resp.status = self.status
resp.body = self.sample_body
@@ -41,8 +18,8 @@ class RequestHandlerTestStatus:
class TestHeaders(helpers.TestSuite):
def prepare(self):
self.on_hello = RequestHandler()
self.api.add_route(self.test_route, self.on_hello)
self.reqhandler = helpers.RequestHandler()
self.api.add_route(self.test_route, self.reqhandler)
def test_content_length(self):
self._simulate_request(self.test_route)
@@ -50,7 +27,7 @@ class TestHeaders(helpers.TestSuite):
headers = self.srmock.headers
# Test Content-Length header set
content_length = str(len(self.on_hello.sample_body))
content_length = str(len(self.reqhandler.sample_body))
content_length_header = ('Content-Length', content_length)
self.assertThat(headers, Contains(content_length_header))
@@ -58,16 +35,16 @@ class TestHeaders(helpers.TestSuite):
self._simulate_request(self.test_route)
# Make sure we picked up host from HTTP_HOST, not SERVER_NAME
host = self.on_hello.req.get_header('host')
self.assertThat(host, Equals('falconer'))
host = self.reqhandler.req.try_get_header('host')
self.assertEquals(host, 'falconer')
def test_host_fallback(self):
# Set protocol to 1.0 so that we won't get a host header
self._simulate_request(self.test_route, protocol='HTTP/1.0')
# Make sure we picked up host from HTTP_HOST, not SERVER_NAME
host = self.on_hello.req.get_header('host')
self.assertThat(host, Equals('localhost'))
host = self.reqhandler.req.try_get_header('host')
self.assertEquals(host, 'localhost')
def test_host_fallback_port8000(self):
# Set protocol to 1.0 so that we won't get a host header
@@ -75,8 +52,8 @@ class TestHeaders(helpers.TestSuite):
port='8000')
# Make sure we picked up host from HTTP_HOST, not SERVER_NAME
host = self.on_hello.req.get_header('host')
self.assertThat(host, Equals('localhost:8000'))
host = self.reqhandler.req.try_get_header('host')
self.assertEquals(host, 'localhost:8000')
def test_no_body_on_1xx(self):
self.request_handler = RequestHandlerTestStatus(falcon.HTTP_102)
@@ -86,7 +63,7 @@ class TestHeaders(helpers.TestSuite):
self.assertThat(self.srmock.headers_dict,
Not(Contains('Content-Length')))
self.assertThat(body, Equals([]))
self.assertEquals(body, [])
def test_no_body_on_101(self):
self.request_handler = RequestHandlerTestStatus(falcon.HTTP_101)
@@ -96,7 +73,7 @@ class TestHeaders(helpers.TestSuite):
self.assertThat(self.srmock.headers_dict,
Not(Contains('Content-Length')))
self.assertThat(body, Equals([]))
self.assertEquals(body, [])
def test_no_body_on_204(self):
self.request_handler = RequestHandlerTestStatus(falcon.HTTP_204)
@@ -106,7 +83,7 @@ class TestHeaders(helpers.TestSuite):
self.assertThat(self.srmock.headers_dict,
Not(Contains('Content-Length')))
self.assertThat(body, Equals([]))
self.assertEquals(body, [])
def test_no_body_on_304(self):
self.request_handler = RequestHandlerTestStatus(falcon.HTTP_304)
@@ -116,7 +93,7 @@ class TestHeaders(helpers.TestSuite):
self.assertThat(self.srmock.headers_dict,
Not(Contains('Content-Length')))
self.assertThat(body, Equals([]))
self.assertEquals(body, [])
def test_passthrough_req_headers(self):
req_headers = {
@@ -126,13 +103,13 @@ class TestHeaders(helpers.TestSuite):
self._simulate_request(self.test_route, headers=req_headers)
for name, expected_value in req_headers.iteritems():
actual_value = self.on_hello.req.get_header(name)
self.assertThat(actual_value, Equals(expected_value))
actual_value = self.reqhandler.req.try_get_header(name)
self.assertEquals(actual_value, expected_value)
def test_passthrough_resp_headers(self):
self._simulate_request(self.test_route)
resp_headers = self.srmock.headers
for h in self.on_hello.resp_headers.iteritems():
for h in self.reqhandler.resp_headers.iteritems():
self.assertThat(resp_headers, Contains(h))

View File

@@ -1,8 +1,6 @@
import testtools
from testtools.matchers import Equals, MatchesRegex
import falcon
import test.helpers as helpers
import helpers
class HelloRequestHandler:
sample_status = "200 OK"
@@ -11,7 +9,7 @@ class HelloRequestHandler:
def __init__(self):
self.called = False
def __call__(self, ctx, req, resp):
def on_get(self, ctx, req, resp):
self.called = True
self.ctx, self.req, self.resp = ctx, req, resp
@@ -26,18 +24,25 @@ class TestHelloWorld(helpers.TestSuite):
self.on_hello = HelloRequestHandler()
self.api.add_route(self.test_route, self.on_hello)
self.root_reqhandler = helpers.RequestHandler()
self.api.add_route('', self.root_reqhandler)
def test_empty_route(self):
self._simulate_request('')
self.assertTrue(self.root_reqhandler.called)
def test_hello_route_negative(self):
bogus_route = self.test_route + 'x'
self._simulate_request(bogus_route)
# Ensure the request was NOT routed to on_hello
self.assertFalse(self.on_hello.called)
self.assertThat(self.srmock.status, Equals(falcon.HTTP_404))
self.assertEquals(self.srmock.status, falcon.HTTP_404)
def test_hello_route(self):
self._simulate_request(self.test_route)
resp = self.on_hello.resp
self.assertThat(resp.status, Equals(self.on_hello.sample_status))
self.assertEquals(resp.status, self.on_hello.sample_status)
self.assertThat(resp.body, Equals(self.on_hello.sample_body))
self.assertEquals(resp.body, self.on_hello.sample_body)

View File

@@ -0,0 +1,90 @@
import helpers
import falcon
from testtools.matchers import Contains
HTTP_METHODS = (
'CONNECT',
'DELETE',
'GET',
'HEAD',
'OPTIONS',
'POST',
'PUT',
'TRACE'
)
class RequestHandlerWithGet:
def __init__(self):
self.called = False
def on_get(self, ctx, req, resp):
self.called = True
self.ctx, self.req, self.resp = ctx, req, resp
resp.status = falcon.HTTP_200
class RequestHandlerMisc:
def __init__(self):
self.called = False
def on_get(self, ctx, req, resp):
self.called = True
self.ctx, self.req, self.resp = ctx, req, resp
resp.status = falcon.HTTP_204
def on_head(self, ctx, req, resp):
self.called = True
self.ctx, self.req, self.resp = ctx, req, resp
resp.status = falcon.HTTP_204
def on_put(self, ctx, req, resp):
self.called = True
self.ctx, self.req, self.resp = ctx, req, resp
resp.status = falcon.HTTP_400
class TestHttpMethodRouting(helpers.TestSuite):
def prepare(self):
self.reqhandler_get = RequestHandlerWithGet()
self.api.add_route('/get', self.reqhandler_get)
self.reqhandler_misc = RequestHandlerMisc()
self.api.add_route('/misc', self.reqhandler_misc)
def test_get(self):
self._simulate_request('/get')
self.assertTrue(self.reqhandler_get.called)
def test_misc(self):
for method in ['GET', 'HEAD', 'PUT']:
self.reqhandler_misc.called = False
self._simulate_request('/misc', method=method)
self.assertTrue(self.reqhandler_misc.called)
self.assertEquals(self.reqhandler_misc.req.method, method)
def test_method_not_allowed(self):
for method in HTTP_METHODS:
if method == 'GET':
continue
self.reqhandler_get.called = False
self._simulate_request('/get', method=method)
self.assertFalse(self.reqhandler_get.called)
self.assertEquals(self.srmock.status, '405 Method Not Allowed')
headers = self.srmock.headers
allow_header = ('Allow', 'GET')
self.assertThat(headers, Contains(allow_header))
def test_bogus_method(self):
self._simulate_request('/get', method=self.getUniqueString())
self.assertFalse(self.reqhandler_get.called)

43
test/test_query_params.py Normal file
View File

@@ -0,0 +1,43 @@
import testtools
import test.helpers as helpers
class TestQueryParams(helpers.TestSuite):
def prepare(self):
self.reqhandler = helpers.RequestHandler()
self.api.add_route('/', self.reqhandler)
def test_none(self):
query_string = ''
self._simulate_request('/', query_string=query_string)
req = self.reqhandler.req
self.assertEquals(req.try_get_param('marker'), None)
self.assertEquals(req.try_get_param('limit'), None)
def test_simple(self):
query_string = 'marker=deadbeef&limit=25'
self._simulate_request('/', query_string=query_string)
req = self.reqhandler.req
self.assertEquals(req.try_get_param('marker'), 'deadbeef')
self.assertEquals(req.try_get_param('limit'), '25')
def test_list_type(self):
query_string = 'colors=red,green,blue&limit=1'
self._simulate_request('/', query_string=query_string)
req = self.reqhandler.req
self.assertEquals(req.try_get_param('colors'), ['red', 'green', 'blue'])
self.assertEquals(req.try_get_param('limit'), '1')
def test_bogus_input(self):
query_string = 'colors=red,green,&limit=1&pickle'
self._simulate_request('/', query_string=query_string)
req = self.reqhandler.req
self.assertEquals(req.try_get_param('colors'), ['red', 'green', ''])
self.assertEquals(req.try_get_param('limit'), '1')
self.assertEquals(req.try_get_param('pickle'), None)

46
test/test_req_vars.py Normal file
View File

@@ -0,0 +1,46 @@
from falcon.request import Request
import test.helpers as helpers
class TestReqVars(helpers.TestSuite):
def prepare(self):
qs = '?marker=deadbeef&limit=10'
headers = {
'Content-Type': 'text/plain',
'Content-Length': '4829'
}
self.req = Request(helpers.create_environ(script='/test',
path='/hello',
query_string=qs,
headers=headers))
def test_reconstruct_url(self):
req = self.req
scheme = req.protocol
host = req.try_get_header('host')
app = req.app
path = req.path
query_string = req.query_string
expected_url = 'http://falconer/test/hello?marker=deadbeef&limit=10'
actual_url = ''.join([scheme, '://', host, app, path, query_string])
self.assertEquals(actual_url, expected_url)
def test_method(self):
self.assertEquals(self.req.method, 'GET')
self.req = Request(helpers.create_environ(path='', method='HEAD'))
self.assertEquals(self.req.method, 'HEAD')
def test_empty_path(self):
self.req = Request(helpers.create_environ(path=''))
self.assertEquals(self.req.path, '/')
def test_content_type(self):
self.assertEquals(self.req.try_get_header('content-type'), 'text/plain')
def test_content_length(self):
self.assertEquals(self.req.try_get_header('content-length'), '4829')

View File

@@ -0,0 +1,58 @@
import testtools
from testtools.matchers import Equals, MatchesRegex, Contains, Not
from falcon.request import Request
import test.helpers as helpers
class TestReqVars(helpers.TestSuite):
def prepare(self):
qs = '?marker=deadbeef&limit=10'
headers = {
'Content-Type': 'text/plain',
'Content-Length': '4829'
}
self.req = Request(helpers.create_environ(script='/test',
path='/hello',
query_string=qs,
headers=headers))
def test_reconstruct_url(self):
req = self.req
scheme = req.protocol
host = req.get_header('host')
app = req.app
path = req.path
query_string = req.query_string
expected_url = 'http://falconer/test/hello?marker=deadbeef&limit=10'
actual_url = ''.join([scheme, '://', host, app, path, query_string])
self.assertThat(actual_url, Equals(expected_url))
def test_method(self):
self.assertEquals(self.req.method, 'GET')
self.req = Request(helpers.create_environ(path='', method='HEAD'))
self.assertEquals(self.req.method, 'HEAD')
def test_empty_path(self):
self.req = Request(helpers.create_environ(path=''))
self.assertEquals(self.req.path, '/')
def test_content_type(self):
self.assertEquals(self.req.get_header('content-type'), 'text/plain')
def test_content_length(self):
<<<<<<< Updated upstream
self.assertThat(self.req.get_header('content-length'),
Equals('4829'))
def test_http_request_method(self):
self.assertThat(self.req.method, Equals('GET'))
=======
self.assertEquals(self.req.get_header('content-length'), '4829')
>>>>>>> Stashed changes

48
test/test_request_body.py Normal file
View File

@@ -0,0 +1,48 @@
import testtools
import helpers
class TestRequestBody(helpers.TestSuite):
def prepare(self):
self.reqhandler = helpers.RequestHandler()
self.api.add_route('/', self.reqhandler)
def test_empty_body(self):
self._simulate_request('/', body='')
stream = self.reqhandler.req.body
stream.seek(0, 2)
self.assertEquals(stream.tell(), 0)
def test_tiny_body(self):
expected_body = '.'
self._simulate_request('', body=expected_body)
stream = self.reqhandler.req.body
actual_body = stream.read(1)
self.assertEquals(actual_body, expected_body)
stream.seek(0, 2)
self.assertEquals(stream.tell(), 1)
def test_read_body(self):
expected_body = helpers.rand_string(2, 1 * 1024 * 1024)
expected_len = len(expected_body)
headers = {'Content-Length': str(expected_len)}
self._simulate_request('', body=expected_body, headers=headers)
content_len = self.reqhandler.req.try_get_header('content-length')
self.assertEqual(content_len, str(expected_len))
stream = self.reqhandler.req.body
actual_body = stream.read()
self.assertEquals(actual_body, expected_body)
stream.seek(0, 2)
self.assertEquals(stream.tell(), expected_len)
self.assertEquals(stream.tell(), expected_len)

View File

@@ -0,0 +1,61 @@
import test.helpers as helpers
class TestUriTemplates(helpers.TestSuite):
def prepare(self):
self.reqhandler = helpers.RequestHandler()
def test_root_path(self):
self.api.add_route('/', self.reqhandler)
self._simulate_request('/')
self.assertTrue(self.reqhandler.called)
req = self.reqhandler.req
self.assertEquals(req.try_get_param('id'), None)
def test_no_vars(self):
self.api.add_route('/hello/world', self.reqhandler)
self._simulate_request('/hello/world')
self.assertTrue(self.reqhandler.called)
req = self.reqhandler.req
self.assertEquals(req.try_get_param('world'), None)
def test_single(self):
self.api.add_route('/widgets/{id}', self.reqhandler)
self._simulate_request('/widgets/123')
self.assertTrue(self.reqhandler.called)
req = self.reqhandler.req
self.assertEquals(req.try_get_param('id'), '123')
self.assertEquals(req.try_get_param('Id'), None)
def test_single_trailing_slash(self):
self.api.add_route('/widgets/{id}/', self.reqhandler)
self._simulate_request('/widgets/123')
self.assertFalse(self.reqhandler.called)
self._simulate_request('/widgets/123/')
self.assertTrue(self.reqhandler.called)
req = self.reqhandler.req
self.assertEquals(req.try_get_param('id'), '123')
def test_multiple(self):
self.api.add_route('/messages/{Id}/names/{Name}', self.reqhandler)
test_id = self.getUniqueString()
test_name = self.getUniqueString()
path = '/messages/' + test_id + '/names/' + test_name
self._simulate_request(path)
self.assertTrue(self.reqhandler.called)
req = self.reqhandler.req
self.assertEquals(req.try_get_param('Id'), test_id)
self.assertEquals(req.try_get_param('Name'), test_name)
self.assertEquals(req.try_get_param('name'), None)

View File

@@ -4,6 +4,7 @@ from testtools.matchers import Equals, MatchesRegex
import falcon
import test.helpers as helpers
def _is_iterable(thing):
try:
for i in thing:
@@ -13,6 +14,7 @@ def _is_iterable(thing):
except:
return False
class TestWsgi(testtools.TestCase):
def test_pep333(self):