diff --git a/.appveyor.yml b/.appveyor.yml new file mode 100644 index 00000000..368270c5 --- /dev/null +++ b/.appveyor.yml @@ -0,0 +1,32 @@ +version: "{branch}.{build}" + +environment: + matrix: + - TOXENV: py35-no-ext + PYTHON: "C:\\Python35-x64" + PYTHON_VERSION: "3.5.x" + PYTHON_ARCH: "64" + + - TOXENV: py36-no-ext + PYTHON: "C:\\Python36-x64" + PYTHON_VERSION: "3.6.x" + PYTHON_ARCH: "64" + + - TOXENV: py37-no-ext + PYTHON: "C:\\Python37-x64" + PYTHON_VERSION: "3.7.x" + PYTHON_ARCH: "64" + +init: SET "PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" + +install: + - pip install tox + +build: off + +test_script: tox + +notifications: + - provider: Email + on_build_success: false + on_build_status_changed: false diff --git a/README.rst b/README.rst index 31f036b0..311be757 100644 --- a/README.rst +++ b/README.rst @@ -1,7 +1,7 @@ Sanic ===== -|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |Codecov| |PyPI| |PyPI version| |Code style black| +|Join the chat at https://gitter.im/sanic-python/Lobby| |Build Status| |AppVeyor Build Status| |Documentation| |Codecov| |PyPI| |PyPI version| |Code style black| Sanic is a Flask-like Python 3.5+ web server that's written to go fast. It's based on the work done by the amazing folks at magicstack, and was inspired by `this article `_. @@ -51,6 +51,8 @@ Documentation :target: https://codecov.io/gh/huge-success/sanic .. |Build Status| image:: https://travis-ci.org/huge-success/sanic.svg?branch=master :target: https://travis-ci.org/huge-success/sanic +.. |AppVeyor Build Status| image:: https://ci.appveyor.com/api/projects/status/d8pt3ids0ynexi8c/branch/master?svg=true + :target: https://ci.appveyor.com/project/huge-success/sanic .. |Documentation| image:: https://readthedocs.org/projects/sanic/badge/?version=latest :target: http://sanic.readthedocs.io/en/latest/?badge=latest .. |PyPI| image:: https://img.shields.io/pypi/v/sanic.svg diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index c0728627..63153b94 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -8,6 +8,7 @@ A list of Sanic extensions created by the community. - [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress. - [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template. - [Sanic JWT](https://github.com/ahopkins/sanic-jwt): Authentication, JWT, and permission scoping for Sanic. +- [Sanic-JWT-Extended](https://github.com/devArtoria/Sanic-JWT-Extended): Provides extended JWT support for Sanic - [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI. - [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support. - [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper. @@ -31,4 +32,5 @@ A list of Sanic extensions created by the community. - [Sanic-Auth](https://github.com/pyx/sanic-auth): A minimal backend agnostic session-based user authentication mechanism for Sanic. - [Sanic-CookieSession](https://github.com/pyx/sanic-cookiesession): A client-side only, cookie-based session, similar to the built-in session in Flask. - [Sanic-WTF](https://github.com/pyx/sanic-wtf): Sanic-WTF makes using WTForms with Sanic and CSRF (Cross-Site Request Forgery) protection a little bit easier. +- [sanic-script](https://github.com/tim2anna/sanic-script): An extension for Sanic that adds support for writing commands to your application. - [sanic-sse](https://github.com/inn0kenty/sanic_sse): [Server-Sent Events](https://en.wikipedia.org/wiki/Server-sent_events) implementation for Sanic. diff --git a/docs/sanic/logging.md b/docs/sanic/logging.md index 49805d0e..e1231eb9 100644 --- a/docs/sanic/logging.md +++ b/docs/sanic/logging.md @@ -9,17 +9,32 @@ A simple example using default settings would be like this: ```python from sanic import Sanic +from sanic.log import logger +from sanic.response import text app = Sanic('test') @app.route('/') async def test(request): - return response.text('Hello World!') + logger.info('Here is your log') + return text('Hello World!') if __name__ == "__main__": app.run(debug=True, access_log=True) ``` +After the server is running, you can see some messages looks like: +``` +[2018-11-06 21:16:53 +0800] [24622] [INFO] Goin' Fast @ http://127.0.0.1:8000 +[2018-11-06 21:16:53 +0800] [24667] [INFO] Starting worker [24667] +``` + +You can send a request to server and it will print the log messages: +``` +[2018-11-06 21:18:53 +0800] [25685] [INFO] Here is your log +[2018-11-06 21:18:53 +0800] - (sanic.access)[INFO][127.0.0.1:57038]: GET http://localhost:8000/ 200 12 +``` + To use your own logging config, simply use `logging.config.dictConfig`, or pass `log_config` when you initialize `Sanic` app: @@ -49,7 +64,7 @@ By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**: -- root:
+- sanic.root:
Used to log internal messages. - sanic.error:
diff --git a/sanic/request.py b/sanic/request.py index 013a27ea..e775596a 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -85,7 +85,7 @@ class Request(dict): self.transport = transport # Init but do not inhale - self.body = [] + self.body_init() self.parsed_json = None self.parsed_form = None self.parsed_files = None @@ -106,6 +106,15 @@ class Request(dict): return True return False + def body_init(self): + self.body = [] + + def body_push(self, data): + self.body.append(data) + + def body_finish(self): + self.body = b"".join(self.body) + @property def json(self): if self.parsed_json is None: diff --git a/sanic/server.py b/sanic/server.py index 0d0bf29c..dc197941 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -300,7 +300,7 @@ class HttpProtocol(asyncio.Protocol): self.request.stream.put(body) ) return - self.request.body.append(body) + self.request.body_push(body) def on_message_complete(self): # Entire request (headers and whole body) is received. @@ -313,7 +313,7 @@ class HttpProtocol(asyncio.Protocol): self.request.stream.put(None) ) return - self.request.body = b"".join(self.request.body) + self.request.body_finish() self.execute_request_handler() def execute_request_handler(self): diff --git a/sanic/testing.py b/sanic/testing.py index eda52d61..19f87095 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -25,7 +25,7 @@ class SanicTestClient: ) logger.info(url) - conn = aiohttp.TCPConnector(verify_ssl=False) + conn = aiohttp.TCPConnector(ssl=False) async with aiohttp.ClientSession( cookies=cookies, connector=conn ) as session: diff --git a/tests/test_custom_request.py b/tests/test_custom_request.py new file mode 100644 index 00000000..d0ae48e7 --- /dev/null +++ b/tests/test_custom_request.py @@ -0,0 +1,53 @@ +from io import BytesIO + +from sanic import Sanic +from sanic.request import Request +from sanic.response import json_dumps, text + + +class CustomRequest(Request): + __slots__ = ("body_buffer",) + + def body_init(self): + self.body_buffer = BytesIO() + + def body_push(self, data): + self.body_buffer.write(data) + + def body_finish(self): + self.body = self.body_buffer.getvalue() + self.body_buffer.close() + + +def test_custom_request(): + app = Sanic(request_class=CustomRequest) + + @app.route("/post", methods=["POST"]) + async def post_handler(request): + return text("OK") + + @app.route("/get") + async def get_handler(request): + return text("OK") + + payload = {"test": "OK"} + headers = {"content-type": "application/json"} + + request, response = app.test_client.post( + "/post", data=json_dumps(payload), headers=headers + ) + + assert isinstance(request.body_buffer, BytesIO) + assert request.body_buffer.closed + assert request.body == b'{"test":"OK"}' + assert request.json.get("test") == "OK" + assert response.text == "OK" + assert response.status == 200 + + request, response = app.test_client.get("/get") + + assert isinstance(request.body_buffer, BytesIO) + assert request.body_buffer.closed + assert request.body == b"" + assert response.text == "OK" + assert response.status == 200 diff --git a/tests/test_helpers.py b/tests/test_helpers.py index cf859b96..eb08624e 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,4 +1,4 @@ -from sanic.helpers import has_message_body +from sanic import helpers def test_has_message_body(): @@ -11,4 +11,26 @@ def test_has_message_body(): (400, True), ) for status_code, expected in tests: - assert has_message_body(status_code) is expected + assert helpers.has_message_body(status_code) is expected + + +def test_is_entity_header(): + tests = ( + ("allow", True), + ("extension-header", True), + ("", False), + ("test", False), + ) + for header, expected in tests: + assert helpers.is_entity_header(header) is expected + + +def test_is_hop_by_hop_header(): + tests = ( + ("connection", True), + ("upgrade", True), + ("", False), + ("test", False), + ) + for header, expected in tests: + assert helpers.is_hop_by_hop_header(header) is expected diff --git a/tests/test_logging.py b/tests/test_logging.py index 95c55de0..d2b43c1a 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -11,6 +11,7 @@ import sanic from sanic.response import text from sanic.log import LOGGING_CONFIG_DEFAULTS from sanic import Sanic +from sanic.log import logger logging_format = '''module: %(module)s; \ @@ -46,7 +47,7 @@ def test_log(app): def test_logging_defaults(): - reset_logging() + # reset_logging() app = Sanic("test_logging") for fmt in [h.formatter for h in logging.getLogger('sanic.root').handlers]: @@ -60,7 +61,7 @@ def test_logging_defaults(): def test_logging_pass_customer_logconfig(): - reset_logging() + # reset_logging() modified_config = LOGGING_CONFIG_DEFAULTS modified_config['formatters']['generic']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s' @@ -104,6 +105,25 @@ def test_log_connection_lost(app, debug, monkeypatch): assert 'Connection lost before response written @' not in log +def test_logger(caplog): + rand_string = str(uuid.uuid4()) + + app = Sanic() + + @app.get('/') + def log_info(request): + logger.info(rand_string) + return text('hello') + + with caplog.at_level(logging.INFO): + request, response = app.test_client.get('/') + + assert caplog.record_tuples[0] == ('sanic.root', logging.INFO, 'Goin\' Fast @ http://127.0.0.1:42101') + assert caplog.record_tuples[1] == ('sanic.root', logging.INFO, 'http://127.0.0.1:42101/') + assert caplog.record_tuples[2] == ('sanic.root', logging.INFO, rand_string) + assert caplog.record_tuples[-1] == ('sanic.root', logging.INFO, 'Server Stopped') + + def test_logging_modified_root_logger_config(): reset_logging() @@ -113,4 +133,3 @@ def test_logging_modified_root_logger_config(): app = Sanic("test_logging", log_config=modified_config) assert logging.getLogger('sanic.root').getEffectiveLevel() == logging.DEBUG - diff --git a/tests/test_multiprocessing.py b/tests/test_multiprocessing.py index ac45ad61..45d6aaf2 100644 --- a/tests/test_multiprocessing.py +++ b/tests/test_multiprocessing.py @@ -31,6 +31,10 @@ def test_multiprocessing(app): assert len(process_list) == num_workers +@pytest.mark.skipif( + not hasattr(signal, 'SIGALRM'), + reason='SIGALRM is not implemented for this platform', +) def test_multiprocessing_with_blueprint(app): from sanic import Blueprint # Selects a number at random so we can spot check diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 163547cb..e674e451 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -173,7 +173,7 @@ class DelayableSanicTestClient(SanicTestClient): return response -Config.REQUEST_TIMEOUT = 2 +Config.REQUEST_TIMEOUT = 0.6 request_timeout_default_app = Sanic('test_request_timeout_default') request_no_timeout_app = Sanic('test_request_no_timeout') @@ -189,14 +189,14 @@ async def handler2(request): def test_default_server_error_request_timeout(): - client = DelayableSanicTestClient(request_timeout_default_app, None, 3) + client = DelayableSanicTestClient(request_timeout_default_app, None, 2) request, response = client.get('/1') assert response.status == 408 assert response.text == 'Error: Request Timeout' def test_default_server_error_request_dont_timeout(): - client = DelayableSanicTestClient(request_no_timeout_app, None, 1) + client = DelayableSanicTestClient(request_no_timeout_app, None, 0.2) request, response = client.get('/1') assert response.status == 200 assert response.text == 'OK'