diff --git a/.travis.yml b/.travis.yml index c4cc57ca..942a5df2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,13 @@ language: python python: -- '3.5' + - '3.5' install: -- pip install -r requirements.txt -- pip install -r requirements-dev.txt -- python setup.py install -- pip install flake8 -- pip install pytest -before_script: flake8 --max-line-length=120 sanic + - pip install -r requirements.txt + - pip install -r requirements-dev.txt + - python setup.py install + - pip install flake8 + - pip install pytest +before_script: flake8 sanic script: py.test -v tests deploy: provider: pypi diff --git a/docs/blueprints.md b/docs/blueprints.md index c7e6b02c..7a4567ee 100644 --- a/docs/blueprints.md +++ b/docs/blueprints.md @@ -56,9 +56,7 @@ In this example, the registered routes in the `app.router` will look like: ``` ## Middleware -Using blueprints allows you to also register middleware exclusively for that -blueprint, without interfering with other blueprints or routes registered -directly on the application object. +Using blueprints allows you to also register middleware globally. ```python @bp.middleware @@ -75,8 +73,7 @@ async def halt_response(request, response): ``` ## Exceptions -Exceptions can also be applied exclusively to blueprints without interfering -with other blueprints or routes registered on the application object. +Exceptions can also be applied exclusively to blueprints globally. ```python @bp.exception(NotFound) diff --git a/sanic/blueprints.py b/sanic/blueprints.py index ab153b8b..f1aa2afc 100644 --- a/sanic/blueprints.py +++ b/sanic/blueprints.py @@ -77,7 +77,8 @@ class Blueprint: """ """ def register_middleware(middleware): - self.record(lambda s: s.add_middleware(middleware, *args, **kwargs)) + self.record( + lambda s: s.add_middleware(middleware, *args, **kwargs)) return middleware # Detect which way this was called, @middleware or @middleware('AT') diff --git a/sanic/exceptions.py b/sanic/exceptions.py index b532d15f..3ed5ab25 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -44,8 +44,13 @@ class Handler: def default(self, request, exception): if issubclass(type(exception), SanicException): - return text("Error: {}".format(exception), status=getattr(exception, 'status_code', 500)) + return text( + "Error: {}".format(exception), + status=getattr(exception, 'status_code', 500)) elif self.sanic.debug: - return text("Error: {}\nException: {}".format(exception, format_exc()), status=500) + return text( + "Error: {}\nException: {}".format( + exception, format_exc()), status=500) else: - return text("An error occurred while generating the request", status=500) + return text( + "An error occurred while generating the request", status=500) diff --git a/sanic/log.py b/sanic/log.py index 41dd56e4..bd2e499e 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,4 +1,5 @@ import logging -logging.basicConfig(level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") +logging.basicConfig( + level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") log = logging.getLogger(__name__) diff --git a/sanic/request.py b/sanic/request.py index 196be534..31b73ed8 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -42,7 +42,9 @@ class Request: self.headers = headers self.version = version self.method = method - self.query_string = url_parsed.query.decode('utf-8') if url_parsed.query else None + self.query_string = None + if url_parsed.query: + self.query_string = url_parsed.query.decode('utf-8') # Init but do not inhale self.body = None @@ -56,7 +58,7 @@ class Request: if not self.parsed_json: try: self.parsed_json = json_loads(self.body) - except: + except Exception: pass return self.parsed_json @@ -66,14 +68,19 @@ class Request: if self.parsed_form is None: self.parsed_form = {} self.parsed_files = {} - content_type, parameters = parse_header(self.headers.get('Content-Type')) + content_type, parameters = parse_header( + self.headers.get('Content-Type')) try: - if content_type is None or content_type == 'application/x-www-form-urlencoded': - self.parsed_form = RequestParameters(parse_qs(self.body.decode('utf-8'))) + is_url_encoded = ( + content_type == 'application/x-www-form-urlencoded') + if content_type is None or is_url_encoded: + self.parsed_form = RequestParameters( + parse_qs(self.body.decode('utf-8'))) elif content_type == 'multipart/form-data': # TODO: Stream this instead of reading to/from memory boundary = parameters['boundary'].encode('utf-8') - self.parsed_form, self.parsed_files = parse_multipart_form(self.body, boundary) + self.parsed_form, self.parsed_files = ( + parse_multipart_form(self.body, boundary)) except Exception as e: log.exception(e) pass @@ -91,7 +98,8 @@ class Request: def args(self): if self.parsed_args is None: if self.query_string: - self.parsed_args = RequestParameters(parse_qs(self.query_string)) + self.parsed_args = RequestParameters( + parse_qs(self.query_string)) else: self.parsed_args = {} @@ -128,7 +136,8 @@ def parse_multipart_form(body, boundary): colon_index = form_line.index(':') form_header_field = form_line[0:colon_index] - form_header_value, form_parameters = parse_header(form_line[colon_index + 2:]) + form_header_value, form_parameters = parse_header( + form_line[colon_index + 2:]) if form_header_field == 'Content-Disposition': if 'filename' in form_parameters: @@ -139,7 +148,8 @@ def parse_multipart_form(body, boundary): post_data = form_part[line_index:-4] if file_name or file_type: - files[field_name] = File(type=file_type, name=file_name, body=post_data) + files[field_name] = File( + type=file_type, name=file_name, body=post_data) else: fields[field_name] = post_data.decode('utf-8') diff --git a/sanic/response.py b/sanic/response.py index 3ca5a9ad..2bf9b167 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -19,7 +19,8 @@ STATUS_CODES = { class HTTPResponse: __slots__ = ('body', 'status', 'content_type', 'headers') - def __init__(self, body=None, status=200, headers=None, content_type='text/plain', body_bytes=b''): + def __init__(self, body=None, status=200, headers=None, + content_type='text/plain', body_bytes=b''): self.content_type = content_type if body is not None: @@ -43,7 +44,12 @@ class HTTPResponse: b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) for name, value in self.headers.items() ) - return b'HTTP/%b %d %b\r\nContent-Type: %b\r\nContent-Length: %d\r\nConnection: %b\r\n%b%b\r\n%b' % ( + return (b'HTTP/%b %d %b\r\n' + b'Content-Type: %b\r\n' + b'Content-Length: %d\r\n' + b'Connection: %b\r\n' + b'%b%b\r\n' + b'%b') % ( version.encode(), self.status, STATUS_CODES.get(self.status, b'FAIL'), @@ -62,8 +68,10 @@ def json(body, status=200, headers=None): def text(body, status=200, headers=None): - return HTTPResponse(body, status=status, headers=headers, content_type="text/plain; charset=utf-8") + return HTTPResponse(body, status=status, headers=headers, + content_type="text/plain; charset=utf-8") def html(body, status=200, headers=None): - return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8") + return HTTPResponse(body, status=status, headers=headers, + content_type="text/html; charset=utf-8") diff --git a/sanic/router.py b/sanic/router.py index cbae0292..e6c580d7 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -14,17 +14,19 @@ class Router: def my_route(request, my_parameter): do stuff... - Parameters will be passed as keyword arguments to the request handling function provided - Parameters can also have a type by appending :type to the . If no type is provided, - a string is expected. A regular expression can also be passed in as the type + Parameters will be passed as keyword arguments to the request handling + function provided Parameters can also have a type by appending :type to + the . If no type is provided, a string is expected. A regular + expression can also be passed in as the type TODO: This probably needs optimization for larger sets of routes, - since it checks every route until it finds a match which is bad and I should feel bad + since it checks every route until it finds a match which is bad and + I should feel bad """ routes = None regex_types = { - "string": (None, "\w+"), + "string": (None, "[^/]+"), "int": (int, "\d+"), "number": (float, "[0-9\\.]+"), "alpha": (None, "[A-Za-z]+"), @@ -37,13 +39,17 @@ class Router: """ Adds a handler to the route list :param uri: Path to match - :param methods: Array of accepted method names. If none are provided, any method is allowed - :param handler: Request handler function. When executed, it should provide a response object. + :param methods: Array of accepted method names. + If none are provided, any method is allowed + :param handler: Request handler function. + When executed, it should provide a response object. :return: Nothing """ # Dict for faster lookups of if method allowed - methods_dict = {method: True for method in methods} if methods else None + methods_dict = None + if methods: + methods_dict = {method: True for method in methods} parameters = [] @@ -71,12 +77,15 @@ class Router: pattern_string = re.sub("<(.+?)>", add_parameter, uri) pattern = re.compile("^{}$".format(pattern_string)) - route = Route(handler=handler, methods=methods_dict, pattern=pattern, parameters=parameters) + route = Route( + handler=handler, methods=methods_dict, pattern=pattern, + parameters=parameters) self.routes.append(route) def get(self, request): """ - Gets a request handler based on the URL of the request, or raises an error + Gets a request handler based on the URL of the request, or raises an + error :param request: Request object :return: handler, arguments, keyword arguments """ @@ -89,14 +98,18 @@ class Router: if match: for index, parameter in enumerate(_route.parameters, start=1): value = match.group(index) - kwargs[parameter.name] = parameter.cast(value) if parameter.cast is not None else value + if parameter.cast: + kwargs[parameter.name] = parameter.cast(value) + else: + kwargs[parameter.name] = value route = _route break if route: if route.methods and request.method not in route.methods: - raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), - status_code=405) + raise InvalidUsage( + "Method {} not allowed for URL {}".format( + request.method, request.url), status_code=405) return route.handler, args, kwargs else: raise NotFound("Requested URL {} not found".format(request.url)) @@ -114,15 +127,20 @@ class SimpleRouter: def add(self, uri, methods, handler): # Dict for faster lookups of method allowed - methods_dict = {method: True for method in methods} if methods else None - self.routes[uri] = Route(handler=handler, methods=methods_dict, pattern=uri, parameters=None) + methods_dict = None + if methods: + methods_dict = {method: True for method in methods} + self.routes[uri] = Route( + handler=handler, methods=methods_dict, pattern=uri, + parameters=None) def get(self, request): route = self.routes.get(request.url) if route: if route.methods and request.method not in route.methods: - raise InvalidUsage("Method {} not allowed for URL {}".format(request.method, request.url), - status_code=405) + raise InvalidUsage( + "Method {} not allowed for URL {}".format( + request.method, request.url), status_code=405) return route.handler, [], {} else: raise NotFound("Requested URL {} not found".format(request.url)) diff --git a/sanic/sanic.py b/sanic/sanic.py index 154aac15..f67edc7b 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -102,10 +102,12 @@ class Sanic: async def handle_request(self, request, response_callback): """ - Takes a request from the HTTP Server and returns a response object to be sent back - The HTTP Server only expects a response object, so exception handling must be done here + Takes a request from the HTTP Server and returns a response object to + be sent back The HTTP Server only expects a response object, so + exception handling must be done here :param request: HTTP Request object - :param response_callback: Response function to be called with the response as the only argument + :param response_callback: Response function to be called with the + response as the only argument :return: Nothing """ try: @@ -125,7 +127,9 @@ class Sanic: # Fetch handler from router handler, args, kwargs = self.router.get(request) if handler is None: - raise ServerError("'None' was returned while requesting a handler from the router") + raise ServerError( + ("'None' was returned while requesting a " + "handler from the router")) # Run response handler response = handler(request, *args, **kwargs) @@ -149,9 +153,12 @@ class Sanic: response = await response except Exception as e: if self.debug: - response = HTTPResponse("Error while handling error: {}\nStack: {}".format(e, format_exc())) + response = HTTPResponse( + "Error while handling error: {}\nStack: {}".format( + e, format_exc())) else: - response = HTTPResponse("An error occured while handling an error") + response = HTTPResponse( + "An error occured while handling an error") response_callback(response) @@ -159,15 +166,18 @@ class Sanic: # Execution # -------------------------------------------------------------------- # - def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, before_stop=None): + def run(self, host="127.0.0.1", port=8000, debug=False, after_start=None, + before_stop=None): """ - Runs the HTTP Server and listens until keyboard interrupt or term signal. - On termination, drains connections before closing. + Runs the HTTP Server and listens until keyboard interrupt or term + signal. On termination, drains connections before closing. :param host: Address to host on :param port: Port to host on :param debug: Enables debug output (slows server) - :param after_start: Function to be executed after the server starts listening - :param before_stop: Function to be executed when a stop signal is received before it is respected + :param after_start: Function to be executed after the server starts + listening + :param before_stop: Function to be executed when a stop signal is + received before it is respected :return: Nothing """ self.error_handler.debug = True @@ -191,7 +201,9 @@ class Sanic: request_timeout=self.config.REQUEST_TIMEOUT, request_max_size=self.config.REQUEST_MAX_SIZE, ) - except: + except Exception as e: + log.exception( + 'Experienced exception while trying to serve: {}'.format(e)) pass def stop(self): diff --git a/sanic/server.py b/sanic/server.py index 5704bccf..0a10f5fd 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -6,7 +6,7 @@ import httptools try: import uvloop as async_loop -except: +except ImportError: async_loop = asyncio from .log import log @@ -18,12 +18,18 @@ class Signal: class HttpProtocol(asyncio.Protocol): - __slots__ = ('loop', 'transport', 'connections', 'signal', # event loop, connection - 'parser', 'request', 'url', 'headers', # request params - 'request_handler', 'request_timeout', 'request_max_size', # request config - '_total_request_size', '_timeout_handler') # connection management + __slots__ = ( + # event loop, connection + 'loop', 'transport', 'connections', 'signal', + # request params + 'parser', 'request', 'url', 'headers', + # request config + 'request_handler', 'request_timeout', 'request_max_size', + # connection management + '_total_request_size', '_timeout_handler') - def __init__(self, *, loop, request_handler, signal=Signal(), connections={}, request_timeout=60, + def __init__(self, *, loop, request_handler, signal=Signal(), + connections={}, request_timeout=60, request_max_size=None): self.loop = loop self.transport = None @@ -46,7 +52,8 @@ class HttpProtocol(asyncio.Protocol): def connection_made(self, transport): self.connections[self] = True - self._timeout_handler = self.loop.call_later(self.request_timeout, self.connection_timeout) + self._timeout_handler = self.loop.call_later( + self.request_timeout, self.connection_timeout) self.transport = transport def connection_lost(self, exc): @@ -63,10 +70,13 @@ class HttpProtocol(asyncio.Protocol): # -------------------------------------------- # def data_received(self, data): - # Check for the request itself getting too large and exceeding memory limits + # Check for the request itself getting too large and exceeding + # memory limits self._total_request_size += len(data) if self._total_request_size > self.request_max_size: - return self.bail_out("Request too large ({}), connection closed".format(self._total_request_size)) + return self.bail_out( + "Request too large ({}), connection closed".format( + self._total_request_size)) # Create parser if this is the first time we're receiving data if self.parser is None: @@ -78,14 +88,16 @@ class HttpProtocol(asyncio.Protocol): try: self.parser.feed_data(data) except httptools.parser.errors.HttpParserError as e: - self.bail_out("Invalid request data, connection closed ({})".format(e)) + self.bail_out( + "Invalid request data, connection closed ({})".format(e)) def on_url(self, url): self.url = url def on_header(self, name, value): if name == b'Content-Length' and int(value) > self.request_max_size: - return self.bail_out("Request body too large ({}), connection closed".format(value)) + return self.bail_out( + "Request body too large ({}), connection closed".format(value)) self.headers.append((name.decode(), value.decode('utf-8'))) @@ -101,7 +113,8 @@ class HttpProtocol(asyncio.Protocol): self.request.body = body def on_message_complete(self): - self.loop.create_task(self.request_handler(self.request, self.write_response)) + self.loop.create_task( + self.request_handler(self.request, self.write_response)) # -------------------------------------------- # # Responding @@ -109,14 +122,18 @@ class HttpProtocol(asyncio.Protocol): def write_response(self, response): try: - keep_alive = self.parser.should_keep_alive() and not self.signal.stopped - self.transport.write(response.output(self.request.version, keep_alive, self.request_timeout)) + keep_alive = all( + [self.parser.should_keep_alive(), self.signal.stopped]) + self.transport.write( + response.output( + self.request.version, keep_alive, self.request_timeout)) if not keep_alive: self.transport.close() else: self.cleanup() except Exception as e: - self.bail_out("Writing request failed, connection closed {}".format(e)) + self.bail_out( + "Writing request failed, connection closed {}".format(e)) def bail_out(self, message): log.error(message) @@ -140,7 +157,8 @@ class HttpProtocol(asyncio.Protocol): return False -def serve(host, port, request_handler, after_start=None, before_stop=None, debug=False, request_timeout=60, +def serve(host, port, request_handler, after_start=None, before_stop=None, + debug=False, request_timeout=60, request_max_size=None): # Create Event Loop loop = async_loop.new_event_loop() @@ -161,12 +179,9 @@ def serve(host, port, request_handler, after_start=None, before_stop=None, debug ), host, port) try: http_server = loop.run_until_complete(server_coroutine) - except OSError as e: + except Exception as e: log.error("Unable to start server: {}".format(e)) return - except: - log.exception("Unable to start server") - return # Run the on_start function if provided if after_start: diff --git a/tests/test_routes.py b/tests/test_routes.py index c1885271..640f3422 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -39,6 +39,11 @@ def test_dynamic_route_string(): assert response.text == 'OK' assert results[0] == 'test123' + request, response = sanic_endpoint_test(app, uri='/folder/favicon.ico') + + assert response.text == 'OK' + assert results[1] == 'favicon.ico' + def test_dynamic_route_int(): app = Sanic('test_dynamic_route_int')