From 0464d31a9c91f70699b3ad5706f82927dc442623 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Sat, 10 Dec 2016 12:16:37 +0100 Subject: [PATCH 01/25] Find URL encoded filenames on the fs by decoding them first --- sanic/static.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sanic/static.py b/sanic/static.py index 72361a9a..ed7d6f8c 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -2,6 +2,7 @@ from aiofiles.os import stat from os import path from re import sub from time import strftime, gmtime +from urllib.parse import unquote from .exceptions import FileNotFound, InvalidUsage from .response import file, HTTPResponse @@ -38,6 +39,8 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # from herping a derp and treating the uri as an absolute path file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory + + file_path = unquote(file_path) try: headers = {} # Check if the client has been sent this file before From 9ba2f99ea26c366aedea8f94ea0af152fcb43b99 Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Tue, 13 Dec 2016 01:10:24 +0100 Subject: [PATCH 02/25] added a comment on why to decode the file_path --- sanic/static.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sanic/static.py b/sanic/static.py index ed7d6f8c..b02786a4 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -40,6 +40,8 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory + # URL decode the path sent by the browser otherwise we won't be able to + # match filenames which got encoded (filenames with spaces etc) file_path = unquote(file_path) try: headers = {} From 2003eceba19618fcb20d78b19af072726113cdfc Mon Sep 17 00:00:00 2001 From: Paul Jongsma Date: Tue, 13 Dec 2016 10:41:39 +0100 Subject: [PATCH 03/25] remove trailing space --- sanic/static.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/static.py b/sanic/static.py index b02786a4..a70bff2f 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -40,7 +40,7 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ if file_uri else file_or_directory - # URL decode the path sent by the browser otherwise we won't be able to + # URL decode the path sent by the browser otherwise we won't be able to # match filenames which got encoded (filenames with spaces etc) file_path = unquote(file_path) try: From 841125570095693a7fa8af5dbef3bfb62bf68dc5 Mon Sep 17 00:00:00 2001 From: Cadel Watson Date: Fri, 23 Dec 2016 11:08:04 +1100 Subject: [PATCH 04/25] Create documentation for testing server endpoints. Currently the sanic.utils functionality is undocumented. This provides information on the interface as well as a complete example of testing a server endpoint. --- README.md | 1 + docs/testing.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 docs/testing.md diff --git a/README.md b/README.md index 5aded700..e417b4a1 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ if __name__ == "__main__": * [Class Based Views](docs/class_based_views.md) * [Cookies](docs/cookies.md) * [Static Files](docs/static_files.md) + * [Testing](docs/testing.md) * [Deploying](docs/deploying.md) * [Contributing](docs/contributing.md) * [License](LICENSE) diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 00000000..79c719e8 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,51 @@ +# Testing + +Sanic endpoints can be tested locally using the `sanic.utils` module, which +depends on the additional [aiohttp](https://aiohttp.readthedocs.io/en/stable/) +library. The `sanic_endpoint_test` function runs a local server, issues a +configurable request to an endpoint, and returns the result. It takes the +following arguments: + +- `app` An instance of a Sanic app. +- `method` *(default `'get'`)* A string representing the HTTP method to use. +- `uri` *(default `'/'`)* A string representing the endpoint to test. +- `gather_request` *(default `True`)* A boolean which determines whether the + original request will be returned by the function. If set to `True`, the + return value is a tuple of `(request, response)`, if `False` only the + response is returned. +- `loop` *(default `None`)* The event loop to use. +- `debug` *(default `False`)* A boolean which determines whether to run the + server in debug mode. + +The function further takes the `*request_args` and `**request_kwargs`, which +are passed directly to the aiohttp ClientSession request. For example, to +supply data with a GET request, `method` would be `get` and the keyword +argument `params={'value', 'key'}` would be supplied. More information about +the available arguments to aiohttp can be found +[in the documentation for ClientSession](https://aiohttp.readthedocs.io/en/stable/client_reference.html#client-session). + +Below is a complete example of an endpoint test, +using [pytest](http://doc.pytest.org/en/latest/). The test checks that the +`/challenge` endpoint responds to a GET request with a supplied challenge +string. + +```python +import pytest +import aiohttp +from sanic.utils import sanic_endpoint_test + +# Import the Sanic app, usually created with Sanic(__name__) +from external_server import app + +def test_endpoint_challenge(): + # Create the challenge data + request_data = {'challenge': 'dummy_challenge'} + + # Send the request to the endpoint, using the default `get` method + request, response = sanic_endpoint_test(app, + uri='/challenge', + params=request_data) + + # Assert that the server responds with the challenge string + assert response.text == request_data['challenge'] +``` From 5c1ef2c1cfabcadbc7d4527d05d654e7ad23ab17 Mon Sep 17 00:00:00 2001 From: Romano Bodha Date: Fri, 23 Dec 2016 01:42:05 +0100 Subject: [PATCH 05/25] Fixed import error --- docs/class_based_views.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/class_based_views.md b/docs/class_based_views.md index 223304ae..ee410b1d 100644 --- a/docs/class_based_views.md +++ b/docs/class_based_views.md @@ -6,6 +6,7 @@ Sanic has simple class based implementation. You should implement methods(get, p ```python from sanic import Sanic from sanic.views import HTTPMethodView +from sanic.response import text app = Sanic('some_name') From f091d82badc35116a45d59560f85cc6176980780 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 13:12:59 +0100 Subject: [PATCH 06/25] Improvement improvement: support fo binary data as a input. This do that the response process has more performance because not encoding needed. --- sanic/response.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 15130edd..1f86a807 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -79,7 +79,9 @@ class HTTPResponse: self.content_type = content_type if body is not None: - self.body = body.encode('utf-8') + self.body = body + if type(body) is str: + self.body = body.encode('utf-8') else: self.body = body_bytes From 5afae986a0aeae6191a5532a11244cb7cc405f94 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 15:59:04 +0100 Subject: [PATCH 07/25] Apply response Middleware always Response middleware are useful to apply some post-process information, just before send to the user. For example: Add some HTTP headers (security headers, for example), remove "Server" banner (for security reasons) or cookie management. The change is very very simple: although an "request" middleware has produced any response, we'll even apply the response middlewares. --- sanic/sanic.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index 98bb230d..f48b2bd5 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -193,18 +193,18 @@ class Sanic: if isawaitable(response): response = await response - # -------------------------------------------- # - # Response Middleware - # -------------------------------------------- # + # -------------------------------------------- # + # Response Middleware + # -------------------------------------------- # - if self.response_middleware: - for middleware in self.response_middleware: - _response = middleware(request, response) - if isawaitable(_response): - _response = await _response - if _response: - response = _response - break + if self.response_middleware: + for middleware in self.response_middleware: + _response = middleware(request, response) + if isawaitable(_response): + _response = await _response + if _response: + response = _response + break except Exception as e: # -------------------------------------------- # From 3add40625dc1b195c3129c34b88724e1a1869638 Mon Sep 17 00:00:00 2001 From: cr0hn Date: Fri, 23 Dec 2016 16:07:59 +0100 Subject: [PATCH 08/25] Explain how to chain two (or more) middlewares A funny and useful examples about how to chain middlewares. --- docs/middleware.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/middleware.md b/docs/middleware.md index 0b27443c..88d4e535 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -27,3 +27,23 @@ async def handler(request): app.run(host="0.0.0.0", port=8000) ``` + +## Middleware chain + +If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that **no return** any response in your middleware: + +```python +app = Sanic(__name__) + +@app.middleware('response') +async def custom_banner(request, response): + response.headers["Server"] = "Fake-Server" + +@app.middleware('response') +async def prevent_xss(request, response): + response.headers["x-xss-protection"] = "1; mode=block" + +app.run(host="0.0.0.0", port=8000) +``` + +The above code will apply the two middlewares in order. First the middleware **custom_banner** will change the HTTP Response headers *Server* by *Fake-Server*, and the second middleware **prevent_xss** will add the HTTP Headers for prevent Cross-Site-Scripting (XSS) attacks. From cd17a42234c6465f70795f78436e0589badc5a25 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Fri, 23 Dec 2016 09:59:28 -0800 Subject: [PATCH 09/25] Fix some verbage --- docs/middleware.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/middleware.md b/docs/middleware.md index 88d4e535..39930e3e 100644 --- a/docs/middleware.md +++ b/docs/middleware.md @@ -30,7 +30,7 @@ app.run(host="0.0.0.0", port=8000) ## Middleware chain -If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that **no return** any response in your middleware: +If you want to apply the middleware as a chain, applying more than one, is so easy. You only have to be aware that you do **not return** any response in your middleware: ```python app = Sanic(__name__) From 32ea45d403ddb1e1b143ba2fbae0675f32adbc50 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 22 Dec 2016 21:00:57 -0800 Subject: [PATCH 10/25] allow overriding logging.basicConfig --- examples/override_logging.py | 23 +++++++++++++++++++++++ sanic/log.py | 2 -- sanic/sanic.py | 11 +++++++++-- tests/test_logging.py | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 examples/override_logging.py create mode 100644 tests/test_logging.py diff --git a/examples/override_logging.py b/examples/override_logging.py new file mode 100644 index 00000000..25fd78de --- /dev/null +++ b/examples/override_logging.py @@ -0,0 +1,23 @@ +from sanic import Sanic +from sanic.response import text +import json +import logging + +logging_format = "[%(asctime)s] %(process)d-%(levelname)s " +logging_format += "%(module)s::%(funcName)s():l%(lineno)d: " +logging_format += "%(message)s" + +logging.basicConfig( + format=logging_format, + level=logging.DEBUG +) +log = logging.getLogger() + +# Set logger to override default basicConfig +sanic = Sanic(logger=True) +@sanic.route("/") +def test(request): + log.info("received request; responding with 'hey'") + return text("hey") + +sanic.run(host="0.0.0.0", port=8000) diff --git a/sanic/log.py b/sanic/log.py index bd2e499e..3988bf12 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,5 +1,3 @@ import logging -logging.basicConfig( - level=logging.INFO, format="%(asctime)s: %(levelname)s: %(message)s") log = logging.getLogger(__name__) diff --git a/sanic/sanic.py b/sanic/sanic.py index 98bb230d..c79dca43 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -6,10 +6,11 @@ from multiprocessing import Process, Event from signal import signal, SIGTERM, SIGINT from time import sleep from traceback import format_exc +import logging from .config import Config from .exceptions import Handler -from .log import log, logging +from .log import log from .response import HTTPResponse from .router import Router from .server import serve @@ -18,7 +19,13 @@ from .exceptions import ServerError class Sanic: - def __init__(self, name=None, router=None, error_handler=None): + def __init__(self, name=None, router=None, + error_handler=None, logger=None): + if logger is None: + logging.basicConfig( + level=logging.INFO, + format="%(asctime)s: %(levelname)s: %(message)s" + ) if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 00000000..65de28c2 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,33 @@ +import asyncio +from sanic.response import text +from sanic import Sanic +from io import StringIO +from sanic.utils import sanic_endpoint_test +import logging + +logging_format = '''module: %(module)s; \ +function: %(funcName)s(); \ +message: %(message)s''' + +def test_log(): + log_stream = StringIO() + for handler in logging.root.handlers[:]: + logging.root.removeHandler(handler) + logging.basicConfig( + format=logging_format, + level=logging.DEBUG, + stream=log_stream + ) + log = logging.getLogger() + app = Sanic('test_logging', logger=True) + @app.route('/') + def handler(request): + log.info('hello world') + return text('hello') + + request, response = sanic_endpoint_test(app) + log_text = log_stream.getvalue().strip().split('\n')[-3] + assert log_text == "module: test_logging; function: handler(); message: hello world" + +if __name__ =="__main__": + test_log() From 2f0a582aa782f224e59911fa94d2f45c8c761e18 Mon Sep 17 00:00:00 2001 From: Konstantin Hantsov Date: Sat, 24 Dec 2016 10:28:34 +0100 Subject: [PATCH 11/25] Make golang performance test return JSON instead of string --- tests/performance/golang/golang.http.go | 26 +++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/tests/performance/golang/golang.http.go b/tests/performance/golang/golang.http.go index fb13cc8b..5aeedb61 100644 --- a/tests/performance/golang/golang.http.go +++ b/tests/performance/golang/golang.http.go @@ -1,16 +1,30 @@ package main import ( - "fmt" - "os" - "net/http" + "encoding/json" + "net/http" + "os" ) +type TestJSONResponse struct { + Test bool +} + func handler(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hi there, I love %s!", r.URL.Path[1:]) + response := TestJSONResponse{true} + + js, err := json.Marshal(response) + + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(js) } func main() { - http.HandleFunc("/", handler) - http.ListenAndServe(":" + os.Args[1], nil) + http.HandleFunc("/", handler) + http.ListenAndServe(":"+os.Args[1], nil) } From cc982c5a61ef9523e3565b9d3d014f404d4141ec Mon Sep 17 00:00:00 2001 From: cr0hn Date: Sat, 24 Dec 2016 15:24:25 +0100 Subject: [PATCH 12/25] Update response.py Type check by isinstance --- sanic/response.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/response.py b/sanic/response.py index 1f86a807..c09c8dbd 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -80,7 +80,7 @@ class HTTPResponse: if body is not None: self.body = body - if type(body) is str: + if isinstance(body, str): self.body = body.encode('utf-8') else: self.body = body_bytes From 74f305cfb78bc23e2dd647d83e26d658f29d1e19 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 14:06:53 -0800 Subject: [PATCH 13/25] Adds python36 to tox.ini and .travis.yml --- .travis.yml | 1 + tox.ini | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 942a5df2..5e41a68e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: - '3.5' + - '3.6' install: - pip install -r requirements.txt - pip install -r requirements-dev.txt diff --git a/tox.ini b/tox.ini index 258395ed..ecb7ca87 100644 --- a/tox.ini +++ b/tox.ini @@ -1,27 +1,30 @@ [tox] -envlist = py35, report +envlist = py35, py36 [testenv] deps = aiohttp pytest - # pytest-cov coverage commands = - coverage run -m pytest tests {posargs} + coverage run -m pytest -v tests {posargs} mv .coverage .coverage.{envname} -basepython: - py35: python3.5 - whitelist_externals = coverage mv echo +[testenv:flake8] +deps = + flake8 + +commands = + flake8 sanic + [testenv:report] commands = @@ -29,6 +32,3 @@ commands = coverage report coverage html echo "Open file://{toxinidir}/coverage/index.html" - -basepython = - python3.5 \ No newline at end of file From c2622511ce94f4a46599c23e6540128c65daacaf Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Tue, 13 Dec 2016 12:20:16 -0800 Subject: [PATCH 14/25] Raise error if response is malformed. Issue #115 --- sanic/server.py | 6 ++++-- tests/test_requests.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 9340f374..11756005 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -6,6 +6,7 @@ from signal import SIGINT, SIGTERM from time import time from httptools import HttpRequestParser from httptools.parser.errors import HttpParserError +from .exceptions import ServerError try: import uvloop as async_loop @@ -173,8 +174,9 @@ class HttpProtocol(asyncio.Protocol): "Writing error failed, connection closed {}".format(e)) def bail_out(self, message): - log.debug(message) - self.transport.close() + exception = ServerError(message) + self.write_error(exception) + log.error(message) def cleanup(self): self.parser = None diff --git a/tests/test_requests.py b/tests/test_requests.py index 81895c8c..5895e3d5 100644 --- a/tests/test_requests.py +++ b/tests/test_requests.py @@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps from sanic import Sanic from sanic.response import json, text from sanic.utils import sanic_endpoint_test +from sanic.exceptions import ServerError # ------------------------------------------------------------ # @@ -32,6 +33,22 @@ def test_text(): assert response.text == 'Hello' +def test_invalid_response(): + app = Sanic('test_invalid_response') + + @app.exception(ServerError) + def handler_exception(request, exception): + return text('Internal Server Error.', 500) + + @app.route('/') + async def handler(request): + return 'This should fail' + + request, response = sanic_endpoint_test(app) + assert response.status == 500 + assert response.text == "Internal Server Error." + + def test_json(): app = Sanic('test_json') From 29f3c22fede7716cdebd06b8f4f44c48dfb0814e Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:11:12 -0800 Subject: [PATCH 15/25] Rework conditionals to not be inline --- sanic/static.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sanic/static.py b/sanic/static.py index a70bff2f..e39dd76f 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -33,12 +33,14 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # served. os.path.realpath seems to be very slow if file_uri and '../' in file_uri: raise InvalidUsage("Invalid URL") - + # Merge served directory and requested file if provided # Strip all / that in the beginning of the URL to help prevent python # from herping a derp and treating the uri as an absolute path - file_path = path.join(file_or_directory, sub('^[/]*', '', file_uri)) \ - if file_uri else file_or_directory + file_path = file_or_directory + if file_uri: + file_path = path.join( + file_or_directory, sub('^[/]*', '', file_uri)) # URL decode the path sent by the browser otherwise we won't be able to # match filenames which got encoded (filenames with spaces etc) From 16182472fa73b6b5035ce4904bbb3edf3e1bf8a8 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:11:46 -0800 Subject: [PATCH 16/25] Remove trailing whitespace --- sanic/static.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sanic/static.py b/sanic/static.py index e39dd76f..9f5f2d52 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -33,7 +33,6 @@ def register(app, uri, file_or_directory, pattern, use_modified_since): # served. os.path.realpath seems to be very slow if file_uri and '../' in file_uri: raise InvalidUsage("Invalid URL") - # Merge served directory and requested file if provided # Strip all / that in the beginning of the URL to help prevent python # from herping a derp and treating the uri as an absolute path From 8be849cc40dc5f8f55536c462f9dff0c657f6a0f Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:16:19 -0800 Subject: [PATCH 17/25] Rewrite static files tests Relates to PR #188 Changes include: - Rewriting to work with pytest fixtures and an actual static directory - Addition of a test that covers file paths that must be unquoted as a uri --- tests/static/decode me.txt | 1 + tests/static/test.file | 1 + tests/test_static.py | 60 +++++++++++++++++++++++++++++--------- 3 files changed, 48 insertions(+), 14 deletions(-) create mode 100644 tests/static/decode me.txt create mode 100644 tests/static/test.file diff --git a/tests/static/decode me.txt b/tests/static/decode me.txt new file mode 100644 index 00000000..e5d05ac1 --- /dev/null +++ b/tests/static/decode me.txt @@ -0,0 +1 @@ +I need to be decoded as a uri diff --git a/tests/static/test.file b/tests/static/test.file new file mode 100644 index 00000000..0725a6ef --- /dev/null +++ b/tests/static/test.file @@ -0,0 +1 @@ +I am just a regular static file diff --git a/tests/test_static.py b/tests/test_static.py index 6dafac2b..82b0d1f9 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -1,30 +1,62 @@ import inspect import os +import pytest + from sanic import Sanic from sanic.utils import sanic_endpoint_test -def test_static_file(): - current_file = inspect.getfile(inspect.currentframe()) - with open(current_file, 'rb') as file: - current_file_contents = file.read() +@pytest.fixture(scope='module') +def static_file_directory(): + """The static directory to serve""" + current_file = inspect.getfile(inspect.currentframe()) + current_directory = os.path.dirname(os.path.abspath(current_file)) + static_directory = os.path.join(current_directory, 'static') + return static_directory + + +@pytest.fixture(scope='module') +def static_file_path(static_file_directory): + """The path to the static file that we want to serve""" + return os.path.join(static_file_directory, 'test.file') + + +@pytest.fixture(scope='module') +def static_file_content(static_file_path): + """The content of the static file to check""" + with open(static_file_path, 'rb') as file: + return file.read() + + +def test_static_file(static_file_path, static_file_content): app = Sanic('test_static') - app.static('/testing.file', current_file) + app.static('/testing.file', static_file_path) request, response = sanic_endpoint_test(app, uri='/testing.file') assert response.status == 200 - assert response.body == current_file_contents + assert response.body == static_file_content -def test_static_directory(): - current_file = inspect.getfile(inspect.currentframe()) - current_directory = os.path.dirname(os.path.abspath(current_file)) - with open(current_file, 'rb') as file: - current_file_contents = file.read() + +def test_static_directory( + static_file_directory, static_file_path, static_file_content): app = Sanic('test_static') - app.static('/dir', current_directory) + app.static('/dir', static_file_directory) - request, response = sanic_endpoint_test(app, uri='/dir/test_static.py') + request, response = sanic_endpoint_test(app, uri='/dir/test.file') assert response.status == 200 - assert response.body == current_file_contents \ No newline at end of file + assert response.body == static_file_content + + +def test_static_url_decode_file(static_file_directory): + decode_me_path = os.path.join(static_file_directory, 'decode me.txt') + with open(decode_me_path, 'rb') as file: + decode_me_contents = file.read() + + app = Sanic('test_static') + app.static('/dir', static_file_directory) + + request, response = sanic_endpoint_test(app, uri='/dir/decode me.txt') + assert response.status == 200 + assert response.body == decode_me_contents From d7e94473f3c350a1c3ec49e79f3a7e6c711b8842 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:37:55 -0800 Subject: [PATCH 18/25] Use a try/except, it's a bit faster Also reorder some imports and add some comments --- sanic/response.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/sanic/response.py b/sanic/response.py index c09c8dbd..2c4c7f27 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -1,9 +1,11 @@ from aiofiles import open as open_async -from .cookies import CookieJar from mimetypes import guess_type from os import path + from ujson import dumps as json_dumps +from .cookies import CookieJar + COMMON_STATUS_CODES = { 200: b'OK', 400: b'Bad Request', @@ -79,9 +81,12 @@ class HTTPResponse: self.content_type = content_type if body is not None: - self.body = body - if isinstance(body, str): + try: + # Try to encode it regularly self.body = body.encode('utf-8') + except AttributeError: + # Convert it to a str if you can't + self.body = str(body).encode('utf-8') else: self.body = body_bytes From f1f38c24da6d845701801c6f55ba9e5f24fd6acb Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:47:15 -0800 Subject: [PATCH 19/25] Add test for PR: #215 --- tests/test_response.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/test_response.py diff --git a/tests/test_response.py b/tests/test_response.py new file mode 100644 index 00000000..f35f10e9 --- /dev/null +++ b/tests/test_response.py @@ -0,0 +1,18 @@ +from random import choice + +from sanic import Sanic +from sanic.response import HTTPResponse +from sanic.utils import sanic_endpoint_test + + +def test_response_body_not_a_string(): + """Test when a response body sent from the application is not a string""" + app = Sanic('response_body_not_a_string') + random_num = choice(range(1000)) + + @app.route('/hello') + async def hello_route(request): + return HTTPResponse(body=random_num) + + request, response = sanic_endpoint_test(app, uri='/hello') + assert response.text == str(random_num) From cf7616ebe5216007699cb264667079a3c739e29a Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sat, 24 Dec 2016 18:51:16 -0800 Subject: [PATCH 20/25] Increment version to 0.1.9 --- sanic/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/__init__.py b/sanic/__init__.py index 6e7f8d23..6b9b3a80 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from .sanic import Sanic from .blueprints import Blueprint -__version__ = '0.1.8' +__version__ = '0.1.9' __all__ = ['Sanic', 'Blueprint'] From 56d6c2a92910b160a3519c0402721323a21218dc Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sun, 25 Dec 2016 18:55:25 -0800 Subject: [PATCH 21/25] Change travis job to use tox --- .travis.yml | 10 ++-------- tox.ini | 4 +++- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5e41a68e..a215a57b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,14 +2,8 @@ language: python python: - '3.5' - '3.6' -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 sanic -script: py.test -v tests +install: pip install tox-travis +script: tox deploy: provider: pypi user: channelcat diff --git a/tox.ini b/tox.ini index ecb7ca87..222e17c6 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] -envlist = py35, py36 +envlist = py35, py36, flake8, report [testenv] @@ -26,6 +26,8 @@ commands = flake8 sanic [testenv:report] +deps= + coverage commands = coverage combine From 15578547553254728b9cd4f9fd46d92ab4d0ffe0 Mon Sep 17 00:00:00 2001 From: Eli Uriegas Date: Sun, 25 Dec 2016 19:05:11 -0800 Subject: [PATCH 22/25] Update to make flake8 actually work --- .travis.yml | 1 + tox.ini | 27 ++++++++------------------- 2 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index a215a57b..1b31c4f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,3 +1,4 @@ +sudo: false language: python python: - '3.5' diff --git a/tox.ini b/tox.ini index 222e17c6..a2f89206 100644 --- a/tox.ini +++ b/tox.ini @@ -1,22 +1,21 @@ [tox] -envlist = py35, py36, flake8, report +envlist = py35, py36, flake8 + +[travis] + +python = + 3.5: py35, flake8 + 3.6: py36, flake8 [testenv] deps = aiohttp pytest - coverage commands = - coverage run -m pytest -v tests {posargs} - mv .coverage .coverage.{envname} - -whitelist_externals = - coverage - mv - echo + pytest tests {posargs} [testenv:flake8] deps = @@ -24,13 +23,3 @@ deps = commands = flake8 sanic - -[testenv:report] -deps= - coverage - -commands = - coverage combine - coverage report - coverage html - echo "Open file://{toxinidir}/coverage/index.html" From a4f77984b79e52441c07557e05ccc86cd2e82727 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Mon, 26 Dec 2016 14:37:05 -0800 Subject: [PATCH 23/25] stop multiple worker server without sleep loop; issue #73 --- sanic/sanic.py | 5 ++--- tests/test_multiprocessing.py | 26 +++++++++++++++++++++++++- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/sanic/sanic.py b/sanic/sanic.py index ecf5b652..033b1e9d 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -3,8 +3,8 @@ from collections import deque from functools import partial from inspect import isawaitable, stack, getmodulename from multiprocessing import Process, Event +from select import select from signal import signal, SIGTERM, SIGINT -from time import sleep from traceback import format_exc import logging @@ -352,8 +352,7 @@ class Sanic: # Infinitely wait for the stop event try: - while not stop_event.is_set(): - sleep(0.3) + select(stop_event) except: pass diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index 545ecee7..cc967ef1 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -1,5 +1,5 @@ from multiprocessing import Array, Event, Process -from time import sleep +from time import sleep, time from ujson import loads as json_loads from sanic import Sanic @@ -51,3 +51,27 @@ def skip_test_multiprocessing(): raise ValueError("Expected JSON response but got '{}'".format(response)) assert results.get('test') == True + + +def test_drain_connections(): + app = Sanic('test_json') + + @app.route('/') + async def handler(request): + return json({"test": True}) + + stop_event = Event() + async def after_start(*args, **kwargs): + http_response = await local_request('get', '/') + stop_event.set() + + start = time() + app.serve_multiple({ + 'host': HOST, + 'port': PORT, + 'after_start': after_start, + 'request_handler': app.handle_request, + }, workers=2, stop_event=stop_event) + end = time() + + assert end - start < 0.05 From e7314d17753b9d566ab85c66c059a98e1d7f0d6e Mon Sep 17 00:00:00 2001 From: Anton Zhyrney Date: Thu, 29 Dec 2016 19:22:11 +0200 Subject: [PATCH 24/25] fix misprints&renaming --- docs/routing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/routing.md b/docs/routing.md index d15ba4e9..92ac2290 100644 --- a/docs/routing.md +++ b/docs/routing.md @@ -33,12 +33,12 @@ async def handler1(request): return text('OK') app.add_route(handler1, '/test') -async def handler(request, name): +async def handler2(request, name): return text('Folder - {}'.format(name)) -app.add_route(handler, '/folder/') +app.add_route(handler2, '/folder/') -async def person_handler(request, name): +async def person_handler2(request, name): return text('Person - {}'.format(name)) -app.add_route(handler, '/person/') +app.add_route(person_handler2, '/person/') ``` From 0f6ed642daa5294e40bda11024c7c48950f5c4c8 Mon Sep 17 00:00:00 2001 From: Diogo Date: Fri, 30 Dec 2016 07:36:57 -0200 Subject: [PATCH 25/25] created methods to remove a route from api/router --- sanic/router.py | 21 +++++++++ sanic/sanic.py | 3 ++ tests/test_routes.py | 109 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 132 insertions(+), 1 deletion(-) diff --git a/sanic/router.py b/sanic/router.py index 4cc1f073..12f13240 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -23,6 +23,10 @@ class RouteExists(Exception): pass +class RouteDoesNotExist(Exception): + pass + + class Router: """ Router supports basic routing with parameters and method checks @@ -109,6 +113,23 @@ class Router: else: self.routes_static[uri] = route + def remove(self, uri, clean_cache=True): + try: + route = self.routes_all.pop(uri) + except KeyError: + raise RouteDoesNotExist("Route was not registered: {}".format(uri)) + + if route in self.routes_always_check: + self.routes_always_check.remove(route) + elif url_hash(uri) in self.routes_dynamic \ + and route in self.routes_dynamic[url_hash(uri)]: + self.routes_dynamic[url_hash(uri)].remove(route) + else: + self.routes_static.pop(uri) + + if clean_cache: + self._get.cache_clear() + def get(self, request): """ Gets a request handler based on the URL of the request, or raises an diff --git a/sanic/sanic.py b/sanic/sanic.py index ecf5b652..4a78a223 100644 --- a/sanic/sanic.py +++ b/sanic/sanic.py @@ -80,6 +80,9 @@ class Sanic: self.route(uri=uri, methods=methods)(handler) return handler + def remove_route(self, uri, clean_cache=True): + self.router.remove(uri, clean_cache) + # Decorator def exception(self, *exceptions): """ diff --git a/tests/test_routes.py b/tests/test_routes.py index 38591e53..149c71f9 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -2,7 +2,7 @@ import pytest from sanic import Sanic from sanic.response import text -from sanic.router import RouteExists +from sanic.router import RouteExists, RouteDoesNotExist from sanic.utils import sanic_endpoint_test @@ -356,3 +356,110 @@ def test_add_route_method_not_allowed(): request, response = sanic_endpoint_test(app, method='post', uri='/test') assert response.status == 405 + + +def test_remove_static_route(): + app = Sanic('test_remove_static_route') + + async def handler1(request): + return text('OK1') + + async def handler2(request): + return text('OK2') + + app.add_route(handler1, '/test') + app.add_route(handler2, '/test2') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/test2') + assert response.status == 200 + + app.remove_route('/test') + app.remove_route('/test2') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/test2') + assert response.status == 404 + + +def test_remove_dynamic_route(): + app = Sanic('test_remove_dynamic_route') + + async def handler(request, name): + return text('OK') + + app.add_route(handler, '/folder/') + + request, response = sanic_endpoint_test(app, uri='/folder/test123') + assert response.status == 200 + + app.remove_route('/folder/') + request, response = sanic_endpoint_test(app, uri='/folder/test123') + assert response.status == 404 + + +def test_remove_inexistent_route(): + app = Sanic('test_remove_inexistent_route') + + with pytest.raises(RouteDoesNotExist): + app.remove_route('/test') + + +def test_remove_unhashable_route(): + app = Sanic('test_remove_unhashable_route') + + async def handler(request, unhashable): + return text('OK') + + app.add_route(handler, '/folder//end/') + + request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') + assert response.status == 200 + + request, response = sanic_endpoint_test(app, uri='/folder/test/end/') + assert response.status == 200 + + app.remove_route('/folder//end/') + + request, response = sanic_endpoint_test(app, uri='/folder/test/asdf/end/') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/test///////end/') + assert response.status == 404 + + request, response = sanic_endpoint_test(app, uri='/folder/test/end/') + assert response.status == 404 + + +def test_remove_route_without_clean_cache(): + app = Sanic('test_remove_static_route') + + async def handler(request): + return text('OK') + + app.add_route(handler, '/test') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + app.remove_route('/test', clean_cache=True) + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 404 + + app.add_route(handler, '/test') + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200 + + app.remove_route('/test', clean_cache=False) + + request, response = sanic_endpoint_test(app, uri='/test') + assert response.status == 200