diff --git a/falcon/api.py b/falcon/api.py index 33d07a8..79acb49 100644 --- a/falcon/api.py +++ b/falcon/api.py @@ -1,3 +1,6 @@ +from falcon.request import Request +from falcon.response import Response + from falcon.default_request_handlers import * from falcon.status_codes import * @@ -5,25 +8,25 @@ from falcon.status_codes import * # TODO: log exceptions, trace execution, etc. class Api: - """WSGI application implementing a Falcon web API""" + """Provides routing and such for building a web service application""" def __init__(self): self.routes = {} def __call__(self, environ, start_response): + """WSGI protocol handler""" + # PERF: Use literal constructor for dicts - # PERF: Don't use multi-assignment ctx = {} - req = {} - resp = {} - - # TODO: What other things does req need? - req['path'] = path = environ['PATH_INFO'] - # TODO # ctx.update(global_ctx_for_route) + path = environ['PATH_INFO'] + req = Request(path) + + 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, @@ -34,19 +37,36 @@ class Api: try: handler(ctx, req, resp) - except: + except Exception as ex: # TODO pass + resp.set_header('Content-Type', 'text/plain') + + # Consider refactoring into functions, but be careful since that can + # affect performance... + + body = resp.body + content_length = 0 try: - start_response(resp['status'], [('Content-Type', 'text/plain')]) - except: + if body is not None: + content_length = len(body) + except Exception as ex: + #TODO + pass + + resp.set_header('Content-Length', content_length) + headers = resp._wsgi_headers() + + try: + start_response(resp.status, headers) + except Exception as ex: # TODO pass # PERF: Can't predict ratio of empty body to nonempty, so use # "in" which is a good all-around performer. - return [resp['body']] if 'body' in resp else [] + return [body] if body is not None else [] def add_route(self, uri_template, handler): self.routes[uri_template] = handler diff --git a/falcon/default_request_handlers.py b/falcon/default_request_handlers.py index e635cf5..7d3510c 100644 --- a/falcon/default_request_handlers.py +++ b/falcon/default_request_handlers.py @@ -1,4 +1,4 @@ from status_codes import * def path_not_found_handler(ctx, req, resp): - resp['status'] = HTTP_404 + resp.status = HTTP_404 diff --git a/falcon/request.py b/falcon/request.py new file mode 100644 index 0000000..7c43070 --- /dev/null +++ b/falcon/request.py @@ -0,0 +1,5 @@ +class Request: + __slots__ = ('path') + + def __init__(self, path): + self.path = path \ No newline at end of file diff --git a/falcon/response.py b/falcon/response.py new file mode 100644 index 0000000..e879c2d --- /dev/null +++ b/falcon/response.py @@ -0,0 +1,16 @@ +class Response: + __slots__ = ('status', '_headers', 'body', 'stream') + + def __init__(self): + self.status = None + self._headers = {} + self.body = None + self.stream = None + + def set_header(self, name, value): + self._headers[name] = str(value) + + #TODO: Add some helper functions and test them + + def _wsgi_headers(self): + return [t for t in self._headers.items()] \ No newline at end of file diff --git a/test/helpers.py b/test/helpers.py index e56da75..1a4093b 100644 --- a/test/helpers.py +++ b/test/helpers.py @@ -1,4 +1,6 @@ import inspect +import random + import testtools import falcon @@ -32,6 +34,26 @@ class TestSuite(testtools.TestCase): def _simulate_request(self, path): self.api(create_environ(path), self.srmock) +class RandChars: + _chars = 'abcdefghijklqmnopqrstuvwxyz0123456789 \n\t!@#$%^&*()-_=+`~<>,.?/' + + def __init__(self, min, max): + self.target = random.randint(min, max) + self.counter = 0 + + def __iter__(self): + return self + + def next(self): + if self.counter < self.target: + self.counter += 1 + return self._chars[random.randint(0, len(self._chars)-1)] + else: + raise StopIteration + +def rand_string(min, max): + return ''.join([c for c in RandChars(min, max)]) + def create_environ(path='/', query_string=''): return { 'SERVER_SOFTWARE': 'WSGIServer/0.1 Python/2.7.3', diff --git a/test/test_headers.py b/test/test_headers.py index 5b6985d..f6fe4e3 100644 --- a/test/test_headers.py +++ b/test/test_headers.py @@ -1,5 +1,7 @@ import testtools -from testtools.matchers import Equals, MatchesRegex +import random + +from testtools.matchers import Equals, MatchesRegex, Contains import falcon import test.helpers as helpers @@ -7,10 +9,15 @@ import test.helpers as helpers # TODO: Framework adds keep-alive and either chunked or content-length # TODO: Test setting various headers, and seeing that Falcon doesn't override custom ones, but will set them if not present (or not?) # TODO: Test correct content length is set +# TODO: Test calling set_header with bogus arguments +# TODO: The order in which header fields with differing field names are received is not significant. However, it is "good practice" to send general-header fields first, followed by request-header or response- header fields, and ending with the entity-header fields. +# TODO: Helper functions for getting and setting common headers +# TODO: Any default headers, such as content-type? +# TODO: if status is 1xx, 204, or 404 ignore body, don't set content-length class RequestHandler: sample_status = "200 OK" - sample_body = "Hello World!" + sample_body = helpers.rand_string(0, 128 * 1024) def __init__(self): self.called = False @@ -20,8 +27,8 @@ class RequestHandler: self.ctx, self.req, self.resp = ctx, req, resp - resp['status'] = falcon.HTTP_200 - resp['body'] = self.sample_body + resp.status = falcon.HTTP_200 + resp.body = self.sample_body class TestHeaders(helpers.TestSuite): @@ -31,3 +38,10 @@ class TestHeaders(helpers.TestSuite): def test_auto_headers(self): self._simulate_request(self.test_route) + + headers = self.srmock.headers + + content_length = ('Content-Length', str(len(self.on_hello.sample_body))) + self.assertThat(headers, Contains(content_length)) + + diff --git a/test/test_hello.py b/test/test_hello.py index 4178d87..01d0edd 100644 --- a/test/test_hello.py +++ b/test/test_hello.py @@ -16,8 +16,8 @@ class HelloRequestHandler: self.ctx, self.req, self.resp = ctx, req, resp - resp['status'] = falcon.HTTP_200 - resp['body'] = self.sample_body + resp.status = falcon.HTTP_200 + resp.body = self.sample_body class TestHelloWorld(helpers.TestSuite): @@ -38,8 +38,6 @@ class TestHelloWorld(helpers.TestSuite): self._simulate_request(self.test_route) resp = self.on_hello.resp - self.assertTrue('status' in resp) - self.assertThat(resp['status'], Equals(self.on_hello.sample_status)) + self.assertThat(resp.status, Equals(self.on_hello.sample_status)) - self.assertTrue('body' in resp) - self.assertThat(resp['body'], Equals(self.on_hello.sample_body)) + self.assertThat(resp.body, Equals(self.on_hello.sample_body)) diff --git a/test/todo.md b/test/todo.md index bcea073..97193ea 100644 --- a/test/todo.md +++ b/test/todo.md @@ -1,5 +1,7 @@ * Test sending/receiving various status codes +* Ensure req dict has things the app needs, including path, user agent, various headers. +* Test default http status code (200 OK?) * Test compiling routes, throwing on invalid routes (such as missing initial forward slash or non-ascii) * Test setting the body to a stream, rather than a string (and content-length set to chunked?) * Test custom error handlers - customizing error document at least @@ -14,4 +16,5 @@ * Test passing a shared dict to each mock call (e.g., db connections, config)...and that it is passed to the request handler correctly * Test pre/post filters * Test error handling with standard response (for all error classes?) +* Test passing bogus data to create_api and/or add_route