From d76d5e2c5fe6d7e870322cc58c45e349e045e0c4 Mon Sep 17 00:00:00 2001 From: Sergey Fedoruk Date: Sat, 29 Dec 2018 23:56:01 +0100 Subject: [PATCH 01/10] add an option to change access_log using gunicorn --- docs/sanic/deploying.md | 15 ++++++++ sanic/worker.py | 4 +++ tests/test_worker.py | 78 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 97 insertions(+) diff --git a/docs/sanic/deploying.md b/docs/sanic/deploying.md index d3652d0d..b6f84001 100644 --- a/docs/sanic/deploying.md +++ b/docs/sanic/deploying.md @@ -15,6 +15,7 @@ keyword arguments: - `protocol` *(default `HttpProtocol`)*: Subclass of [asyncio.protocol](https://docs.python.org/3/library/asyncio-protocol.html#protocol-classes). +- `access_log` *(default `True`)*: Enables log on handling requests (significantly slows server). ## Workers @@ -63,6 +64,20 @@ of the memory leak. See the [Gunicorn Docs](http://docs.gunicorn.org/en/latest/settings.html#max-requests) for more information. +## Disable debug logging + +To improve the performance add `debug=False` and `access_log=False` in the `run` arguments. + +```python +app.run(host='0.0.0.0', port=1337, workers=4, debug=False, access_log=False) +``` + +Running via Gunicorn you can set `--log-level` higher than `info` to not get any access logs anymore. + +``` +gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning +``` + ## Asynchronous support This is suitable if you *need* to share the sanic process with other applications, in particular the `loop`. However be advised that this method does not support using multiple processes, and is not the preferred way diff --git a/sanic/worker.py b/sanic/worker.py index e6b4f315..474ed0a3 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -57,6 +57,10 @@ class GunicornWorker(base.Worker): if self.app.callable.websocket_enabled else self.http_protocol ) + + # set ACCESS_LOG on base of logging level + self.app.callable.config.ACCESS_LOG = self.log.loglevel <= logging.INFO + self._server_settings = self.app.callable._helper( loop=self.loop, debug=is_debug, diff --git a/tests/test_worker.py b/tests/test_worker.py index 1fad4f28..04596d77 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -4,6 +4,7 @@ import shlex import subprocess import urllib.request from unittest import mock +from urllib.error import HTTPError from sanic.worker import GunicornWorker from sanic.app import Sanic import asyncio @@ -24,12 +25,89 @@ def gunicorn_worker(): worker.kill() +@pytest.fixture(scope='module') +def gunicorn_worker_log_level_info(): + command = ( + 'gunicorn ' + '--bind 127.0.0.1:1338 ' + '--worker-class sanic.worker.GunicornWorker ' + '--log-level info ' + 'examples.simple_server:app' + ) + worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) + time.sleep(2) + return worker + + +@pytest.fixture(scope='module') +def gunicorn_worker_log_level_warning(): + command = ( + 'gunicorn ' + '--bind 127.0.0.1:1339 ' + '--worker-class sanic.worker.GunicornWorker ' + '--log-level warning ' + 'examples.simple_server:app' + ) + worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) + time.sleep(2) + return worker + + +@pytest.fixture(scope='module') +def gunicorn_worker_log_level_warning2(): + command = ( + 'gunicorn ' + '--bind 127.0.0.1:1340 ' + '--worker-class sanic.worker.GunicornWorker ' + '--log-level warning ' + 'examples.exception_monitoring:app' + ) + worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) + time.sleep(2) + return worker + + def test_gunicorn_worker(gunicorn_worker): with urllib.request.urlopen("http://localhost:1337/") as f: res = json.loads(f.read(100).decode()) assert res["test"] +def test_gunicorn_worker_logs_info(gunicorn_worker_log_level_info): + """ + on base of our log-level we get an access message + """ + with urllib.request.urlopen('http://localhost:1338/') as _: + gunicorn_worker_log_level_info.kill() + assert b"(sanic.access)[INFO][127.0.0.1" in gunicorn_worker_log_level_info.stdout.read() + + +def test_gunicorn_worker_logs_warning(gunicorn_worker_log_level_warning): + """ + with log-level warning we are not getting an access messages anymore + """ + with urllib.request.urlopen('http://localhost:1339/') as _: + gunicorn_worker_log_level_warning.kill() + assert not gunicorn_worker_log_level_warning.stdout.read() + + +def test_gunicorn_worker_logs_warning_on_error(gunicorn_worker_log_level_warning2): + """ + with log-level warning we get an error log but don't get an access log + """ + try: + url = urllib.request.urlopen('http://localhost:1340/') + except HTTPError: + pass + else: + url.close() + + gunicorn_worker_log_level_warning2.kill() + log_message = gunicorn_worker_log_level_warning2.stdout.read() + assert b"(sanic.access)[INFO][127.0.0.1" not in log_message + assert b"[ERROR] Exception occurred while handling uri" in log_message + + class GunicornTestWorker(GunicornWorker): def __init__(self): self.app = mock.Mock() From 391fcdc83d54c590066837f36e973ba42c72724f Mon Sep 17 00:00:00 2001 From: Sergey Fedoruk Date: Sun, 30 Dec 2018 19:37:30 +0100 Subject: [PATCH 02/10] fix access_log in run server and fix bool in env variables --- docs/sanic/deploying.md | 10 ++++- sanic/app.py | 16 +++++--- sanic/config.py | 39 +++++++++++------- sanic/worker.py | 3 -- tests/test_config.py | 69 ++++++++++++++++++++++++++++++++ tests/test_keep_alive_timeout.py | 14 +++++-- tests/test_middleware.py | 2 + tests/test_request_timeout.py | 4 +- tests/test_response_timeout.py | 6 ++- tests/test_worker.py | 58 ++++++--------------------- 10 files changed, 143 insertions(+), 78 deletions(-) diff --git a/docs/sanic/deploying.md b/docs/sanic/deploying.md index b6f84001..23dc28b6 100644 --- a/docs/sanic/deploying.md +++ b/docs/sanic/deploying.md @@ -72,10 +72,16 @@ To improve the performance add `debug=False` and `access_log=False` in the `run` app.run(host='0.0.0.0', port=1337, workers=4, debug=False, access_log=False) ``` -Running via Gunicorn you can set `--log-level` higher than `info` to not get any access logs anymore. +Running via Gunicorn you can set Environment variable `SANIC_ACCESS_LOG="False"` ``` -gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning +env SANIC_ACCESS_LOG="False" gunicorn myapp:app --bind 0.0.0.0:1337 --worker-class sanic.worker.GunicornWorker --log-level warning +``` + +Or you can rewrite app config directly + +```python +app.config.ACCESS_LOG = False ``` ## Asynchronous support diff --git a/sanic/app.py b/sanic/app.py index e4e142ca..bd9f7901 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -977,7 +977,7 @@ class Sanic: backlog=100, stop_event=None, register_sys_signals=True, - access_log=True, + access_log=None, **kwargs ): """Run the HTTP Server and listen until keyboard interrupt or term @@ -1027,8 +1027,10 @@ class Sanic: "stop_event will be removed from future versions.", DeprecationWarning, ) - # compatibility old access_log params - self.config.ACCESS_LOG = access_log + # if access_log is passed explicitly change config.ACCESS_LOG + if access_log is not None: + self.config.ACCESS_LOG = access_log + server_settings = self._helper( host=host, port=port, @@ -1086,7 +1088,7 @@ class Sanic: protocol=None, backlog=100, stop_event=None, - access_log=True, + access_log=None, ): """ Asynchronous version of :func:`run`. @@ -1114,8 +1116,10 @@ class Sanic: "stop_event will be removed from future versions.", DeprecationWarning, ) - # compatibility old access_log params - self.config.ACCESS_LOG = access_log + # if access_log is passed explicitly change config.ACCESS_LOG + if access_log is not None: + self.config.ACCESS_LOG = access_log + server_settings = self._helper( host=host, port=port, diff --git a/sanic/config.py b/sanic/config.py index dda377f8..f2c47378 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -12,23 +12,31 @@ BASE_LOGO = """ """ +DEFAULT_CONFIG = { + "REQUEST_MAX_SIZE": 100000000, # 100 megabytes + "REQUEST_BUFFER_QUEUE_SIZE": 100, + "REQUEST_TIMEOUT": 60, # 60 seconds + "RESPONSE_TIMEOUT": 60, # 60 seconds + "KEEP_ALIVE": True, + "KEEP_ALIVE_TIMEOUT": 5, # 5 seconds + "WEBSOCKET_MAX_SIZE": 2 ** 20, # 1 megabytes + "WEBSOCKET_MAX_QUEUE": 32, + "WEBSOCKET_READ_LIMIT": 2 ** 16, + "WEBSOCKET_WRITE_LIMIT": 2 ** 16, + "GRACEFUL_SHUTDOWN_TIMEOUT": 15.0, # 15 sec + "ACCESS_LOG": True, +} + class Config(dict): - def __init__(self, defaults=None, load_env=True, keep_alive=True): + def __init__(self, defaults=None, load_env=True, keep_alive=None): + super().__init__(DEFAULT_CONFIG) super().__init__(defaults or {}) + self.LOGO = BASE_LOGO - self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes - self.REQUEST_BUFFER_QUEUE_SIZE = 100 - self.REQUEST_TIMEOUT = 60 # 60 seconds - self.RESPONSE_TIMEOUT = 60 # 60 seconds - self.KEEP_ALIVE = keep_alive - self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds - self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes - self.WEBSOCKET_MAX_QUEUE = 32 - self.WEBSOCKET_READ_LIMIT = 2 ** 16 - self.WEBSOCKET_WRITE_LIMIT = 2 ** 16 - self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec - self.ACCESS_LOG = True + + if keep_alive is not None: + self.KEEP_ALIVE = keep_alive if load_env: prefix = SANIC_PREFIX if load_env is True else load_env @@ -116,4 +124,7 @@ class Config(dict): try: self[config_key] = float(v) except ValueError: - self[config_key] = v + if v in ['True', 'False']: + self[config_key] = v == 'True' + else: + self[config_key] = v diff --git a/sanic/worker.py b/sanic/worker.py index 474ed0a3..ef8b2128 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -58,9 +58,6 @@ class GunicornWorker(base.Worker): else self.http_protocol ) - # set ACCESS_LOG on base of logging level - self.app.callable.config.ACCESS_LOG = self.log.loglevel <= logging.INFO - self._server_settings = self.app.callable._helper( loop=self.loop, debug=is_debug, diff --git a/tests/test_config.py b/tests/test_config.py index c509b34c..a6ee5229 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -6,6 +6,7 @@ from textwrap import dedent import pytest from sanic import Sanic +from sanic.config import Config, DEFAULT_CONFIG from sanic.exceptions import PyFileError @@ -34,6 +35,13 @@ def test_auto_load_env(): del environ["SANIC_TEST_ANSWER"] +def test_auto_load_bool_env(): + environ["SANIC_TEST_ANSWER"] = "True" + app = Sanic() + assert app.config.TEST_ANSWER == True + del environ["SANIC_TEST_ANSWER"] + + def test_dont_load_env(): environ["SANIC_TEST_ANSWER"] = "42" app = Sanic(load_env=False) @@ -139,3 +147,64 @@ def test_missing_config(app): with pytest.raises(AttributeError) as e: app.config.NON_EXISTENT assert str(e.value) == ("Config has no 'NON_EXISTENT'") + + +def test_config_defaults(): + """ + load DEFAULT_CONFIG + """ + conf = Config() + for key, value in DEFAULT_CONFIG.items(): + assert getattr(conf, key) == value + + +def test_config_custom_defaults(): + """ + we should have all the variables from defaults rewriting them with custom defaults passed in + Config + """ + custom_defaults = { + "REQUEST_MAX_SIZE": 1, + "KEEP_ALIVE": False, + "ACCESS_LOG": False + } + conf = Config(defaults=custom_defaults) + for key, value in DEFAULT_CONFIG.items(): + if key in custom_defaults.keys(): + value = custom_defaults[key] + assert getattr(conf, key) == value + + +def test_config_custom_defaults_with_env(): + """ + test that environment variables has higher priority than DEFAULT_CONFIG and passed defaults dict + """ + custom_defaults = { + "REQUEST_MAX_SIZE123": 1, + "KEEP_ALIVE123": False, + "ACCESS_LOG123": False + } + + environ_defaults = { + "SANIC_REQUEST_MAX_SIZE123": "2", + "SANIC_KEEP_ALIVE123": "True", + "SANIC_ACCESS_LOG123": "False" + } + + for key, value in environ_defaults.items(): + environ[key] = value + + conf = Config(defaults=custom_defaults) + for key, value in DEFAULT_CONFIG.items(): + if "SANIC_" + key in environ_defaults.keys(): + value = environ_defaults["SANIC_" + key] + try: + value = int(value) + except ValueError: + if value in ['True', 'False']: + value = value == 'True' + + assert getattr(conf, key) == value + + for key, value in environ_defaults.items(): + del environ[key] diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index d928a0ea..cef5da5b 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -3,13 +3,17 @@ from sanic import Sanic import asyncio from asyncio import sleep as aio_sleep from sanic.response import text -from sanic.config import Config from sanic import server import aiohttp from aiohttp import TCPConnector from sanic.testing import SanicTestClient, HOST, PORT +CONFIG_FOR_TESTS = { + "KEEP_ALIVE_TIMEOUT": 2, + "KEEP_ALIVE": True +} + class ReuseableTCPConnector(TCPConnector): def __init__(self, *args, **kwargs): super(ReuseableTCPConnector, self).__init__(*args, **kwargs) @@ -141,7 +145,7 @@ class ReuseableSanicTestClient(SanicTestClient): # loop, so the changes above are required too. async def _local_request(self, method, uri, cookies=None, *args, **kwargs): request_keepalive = kwargs.pop( - "request_keepalive", Config.KEEP_ALIVE_TIMEOUT + "request_keepalive", CONFIG_FOR_TESTS['KEEP_ALIVE_TIMEOUT'] ) if uri.startswith(("http:", "https:", "ftp:", "ftps://" "//")): url = uri @@ -191,12 +195,14 @@ class ReuseableSanicTestClient(SanicTestClient): return response -Config.KEEP_ALIVE_TIMEOUT = 2 -Config.KEEP_ALIVE = True keep_alive_timeout_app_reuse = Sanic("test_ka_timeout_reuse") keep_alive_app_client_timeout = Sanic("test_ka_client_timeout") keep_alive_app_server_timeout = Sanic("test_ka_server_timeout") +keep_alive_timeout_app_reuse.config.update(CONFIG_FOR_TESTS) +keep_alive_app_client_timeout.config.update(CONFIG_FOR_TESTS) +keep_alive_app_server_timeout.config.update(CONFIG_FOR_TESTS) + @keep_alive_timeout_app_reuse.route("/1") async def handler1(request): diff --git a/tests/test_middleware.py b/tests/test_middleware.py index 1d0c3e83..a65da8ae 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -73,6 +73,8 @@ def test_middleware_response_exception(app): def test_middleware_response_raise_cancelled_error(app, caplog): + app.config.RESPONSE_TIMEOUT = 1 + @app.middleware("response") async def process_response(request, response): raise CancelledError("CancelledError at response middleware") diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 8cb9bd61..8a060a74 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -3,7 +3,6 @@ from json import JSONDecodeError from sanic import Sanic import asyncio from sanic.response import text -from sanic.config import Config import aiohttp from aiohttp import TCPConnector from sanic.testing import SanicTestClient, HOST @@ -183,9 +182,10 @@ class DelayableSanicTestClient(SanicTestClient): return response -Config.REQUEST_TIMEOUT = 0.6 request_timeout_default_app = Sanic("test_request_timeout_default") request_no_timeout_app = Sanic("test_request_no_timeout") +request_timeout_default_app.config.REQUEST_TIMEOUT = 0.6 +request_no_timeout_app.config.REQUEST_TIMEOUT = 0.6 @request_timeout_default_app.route("/1") diff --git a/tests/test_response_timeout.py b/tests/test_response_timeout.py index f0406724..bae0daa0 100644 --- a/tests/test_response_timeout.py +++ b/tests/test_response_timeout.py @@ -2,13 +2,15 @@ from sanic import Sanic import asyncio from sanic.response import text from sanic.exceptions import ServiceUnavailable -from sanic.config import Config -Config.RESPONSE_TIMEOUT = 1 response_timeout_app = Sanic("test_response_timeout") response_timeout_default_app = Sanic("test_response_timeout_default") response_handler_cancelled_app = Sanic("test_response_handler_cancelled") +response_timeout_app.config.RESPONSE_TIMEOUT = 1 +response_timeout_default_app.config.RESPONSE_TIMEOUT = 1 +response_handler_cancelled_app.config.RESPONSE_TIMEOUT = 1 + @response_timeout_app.route("/1") async def handler_1(request): diff --git a/tests/test_worker.py b/tests/test_worker.py index 04596d77..564f9f67 100644 --- a/tests/test_worker.py +++ b/tests/test_worker.py @@ -4,7 +4,6 @@ import shlex import subprocess import urllib.request from unittest import mock -from urllib.error import HTTPError from sanic.worker import GunicornWorker from sanic.app import Sanic import asyncio @@ -26,12 +25,11 @@ def gunicorn_worker(): @pytest.fixture(scope='module') -def gunicorn_worker_log_level_info(): +def gunicorn_worker_with_access_logs(): command = ( 'gunicorn ' '--bind 127.0.0.1:1338 ' '--worker-class sanic.worker.GunicornWorker ' - '--log-level info ' 'examples.simple_server:app' ) worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) @@ -40,12 +38,13 @@ def gunicorn_worker_log_level_info(): @pytest.fixture(scope='module') -def gunicorn_worker_log_level_warning(): +def gunicorn_worker_with_env_var(): command = ( + 'env SANIC_ACCESS_LOG="False" ' 'gunicorn ' '--bind 127.0.0.1:1339 ' '--worker-class sanic.worker.GunicornWorker ' - '--log-level warning ' + '--log-level info ' 'examples.simple_server:app' ) worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) @@ -53,59 +52,28 @@ def gunicorn_worker_log_level_warning(): return worker -@pytest.fixture(scope='module') -def gunicorn_worker_log_level_warning2(): - command = ( - 'gunicorn ' - '--bind 127.0.0.1:1340 ' - '--worker-class sanic.worker.GunicornWorker ' - '--log-level warning ' - 'examples.exception_monitoring:app' - ) - worker = subprocess.Popen(shlex.split(command), stdout=subprocess.PIPE) - time.sleep(2) - return worker - - def test_gunicorn_worker(gunicorn_worker): with urllib.request.urlopen("http://localhost:1337/") as f: res = json.loads(f.read(100).decode()) assert res["test"] -def test_gunicorn_worker_logs_info(gunicorn_worker_log_level_info): +def test_gunicorn_worker_no_logs(gunicorn_worker_with_env_var): """ - on base of our log-level we get an access message - """ - with urllib.request.urlopen('http://localhost:1338/') as _: - gunicorn_worker_log_level_info.kill() - assert b"(sanic.access)[INFO][127.0.0.1" in gunicorn_worker_log_level_info.stdout.read() - - -def test_gunicorn_worker_logs_warning(gunicorn_worker_log_level_warning): - """ - with log-level warning we are not getting an access messages anymore + if SANIC_ACCESS_LOG was set to False do not show access logs """ with urllib.request.urlopen('http://localhost:1339/') as _: - gunicorn_worker_log_level_warning.kill() - assert not gunicorn_worker_log_level_warning.stdout.read() + gunicorn_worker_with_env_var.kill() + assert not gunicorn_worker_with_env_var.stdout.read() -def test_gunicorn_worker_logs_warning_on_error(gunicorn_worker_log_level_warning2): +def test_gunicorn_worker_with_logs(gunicorn_worker_with_access_logs): """ - with log-level warning we get an error log but don't get an access log + default - show access logs """ - try: - url = urllib.request.urlopen('http://localhost:1340/') - except HTTPError: - pass - else: - url.close() - - gunicorn_worker_log_level_warning2.kill() - log_message = gunicorn_worker_log_level_warning2.stdout.read() - assert b"(sanic.access)[INFO][127.0.0.1" not in log_message - assert b"[ERROR] Exception occurred while handling uri" in log_message + with urllib.request.urlopen('http://localhost:1338/') as _: + gunicorn_worker_with_access_logs.kill() + assert b"(sanic.access)[INFO][127.0.0.1" in gunicorn_worker_with_access_logs.stdout.read() class GunicornTestWorker(GunicornWorker): From 0c5c6dff8f66ace1648d3071235a977e95bf8838 Mon Sep 17 00:00:00 2001 From: Sergey Fedoruk Date: Sun, 30 Dec 2018 20:01:02 +0100 Subject: [PATCH 03/10] fix linting --- sanic/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index f2c47378..60ad4eaf 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -124,7 +124,7 @@ class Config(dict): try: self[config_key] = float(v) except ValueError: - if v in ['True', 'False']: - self[config_key] = v == 'True' + if v in ["True", "False"]: + self[config_key] = v == "True" else: self[config_key] = v From 9d4d15ddc74fc691e014955c936e64f4a85b0cf6 Mon Sep 17 00:00:00 2001 From: Sergey Fedoruk Date: Sun, 30 Dec 2018 21:27:20 +0100 Subject: [PATCH 04/10] add config tests --- tests/test_config.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index a6ee5229..1ace7579 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -208,3 +208,32 @@ def test_config_custom_defaults_with_env(): for key, value in environ_defaults.items(): del environ[key] + + +def test_config_access_log_passing_in_run(app): + assert app.config.ACCESS_LOG == True + + @app.listener('after_server_start') + async def _request(sanic, loop): + app.stop() + + app.run(port=1340, access_log=False) + assert app.config.ACCESS_LOG == False + + app.run(port=1340, access_log=True) + assert app.config.ACCESS_LOG == True + + +def test_config_rewrite_keep_alive(): + config = Config() + assert config.KEEP_ALIVE == DEFAULT_CONFIG["KEEP_ALIVE"] + config = Config(keep_alive=True) + assert config.KEEP_ALIVE == True + config = Config(keep_alive=False) + assert config.KEEP_ALIVE == False + + # use defaults + config = Config(defaults={"KEEP_ALIVE": False}) + assert config.KEEP_ALIVE == False + config = Config(defaults={"KEEP_ALIVE": True}) + assert config.KEEP_ALIVE == True From 74f05108d7f09d52ac4c60330e12978f6943fe96 Mon Sep 17 00:00:00 2001 From: Sergey Fedoruk Date: Sun, 30 Dec 2018 22:14:10 +0100 Subject: [PATCH 05/10] async test for access_log in create_server --- tests/test_config.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/test_config.py b/tests/test_config.py index 1ace7579..fe4b97b9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -224,6 +224,20 @@ def test_config_access_log_passing_in_run(app): assert app.config.ACCESS_LOG == True +async def test_config_access_log_passing_in_create_server(app): + assert app.config.ACCESS_LOG == True + + @app.listener('after_server_start') + async def _request(sanic, loop): + app.stop() + + await app.create_server(port=1341, access_log=False) + assert app.config.ACCESS_LOG == False + + await app.create_server(port=1342, access_log=True) + assert app.config.ACCESS_LOG == True + + def test_config_rewrite_keep_alive(): config = Config() assert config.KEEP_ALIVE == DEFAULT_CONFIG["KEEP_ALIVE"] From 0b728ade3ac5697f16666324e1e9ca5198094b89 Mon Sep 17 00:00:00 2001 From: Sergey Fedoruk Date: Sun, 30 Dec 2018 22:56:02 +0100 Subject: [PATCH 06/10] change Config.__init__ --- sanic/config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 60ad4eaf..5214f8f5 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -30,8 +30,8 @@ DEFAULT_CONFIG = { class Config(dict): def __init__(self, defaults=None, load_env=True, keep_alive=None): - super().__init__(DEFAULT_CONFIG) - super().__init__(defaults or {}) + defaults = defaults or {} + super().__init__({**DEFAULT_CONFIG, **defaults}) self.LOGO = BASE_LOGO From a86a10b1285920bcdd852ac98c5158e65c9ae89b Mon Sep 17 00:00:00 2001 From: Sergey Fedoruk Date: Mon, 31 Dec 2018 09:49:53 +0100 Subject: [PATCH 07/10] add control of access_log argument type --- sanic/app.py | 17 +++++++++++++++-- tests/test_config.py | 10 +++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index bd9f7901..eb35e9ff 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -1029,7 +1029,12 @@ class Sanic: ) # if access_log is passed explicitly change config.ACCESS_LOG if access_log is not None: - self.config.ACCESS_LOG = access_log + if isinstance(access_log, bool): + self.config.ACCESS_LOG = access_log + else: + raise ServerError( + ("'access_log' passed in 'run' should be boolean") + ) server_settings = self._helper( host=host, @@ -1118,7 +1123,15 @@ class Sanic: ) # if access_log is passed explicitly change config.ACCESS_LOG if access_log is not None: - self.config.ACCESS_LOG = access_log + if isinstance(access_log, bool): + self.config.ACCESS_LOG = access_log + else: + raise ServerError( + ( + "'access_log' passed in 'create_server' " + "should be boolean" + ) + ) server_settings = self._helper( host=host, diff --git a/tests/test_config.py b/tests/test_config.py index fe4b97b9..575800e3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,7 +7,7 @@ import pytest from sanic import Sanic from sanic.config import Config, DEFAULT_CONFIG -from sanic.exceptions import PyFileError +from sanic.exceptions import PyFileError, ServerError @contextmanager @@ -223,6 +223,10 @@ def test_config_access_log_passing_in_run(app): app.run(port=1340, access_log=True) assert app.config.ACCESS_LOG == True + with pytest.raises(ServerError) as e: + app.run(port=1340, access_log='string') + assert str(e.value) == ("'access_log' passed in 'run' should be boolean") + async def test_config_access_log_passing_in_create_server(app): assert app.config.ACCESS_LOG == True @@ -237,6 +241,10 @@ async def test_config_access_log_passing_in_create_server(app): await app.create_server(port=1342, access_log=True) assert app.config.ACCESS_LOG == True + with pytest.raises(ServerError) as e: + await app.create_server(port=1343, access_log='somestring') + assert str(e.value) == ("'access_log' passed in 'create_server' should be boolean") + def test_config_rewrite_keep_alive(): config = Config() From b7a6f36e950182c2432286e7f7a46ac8aa2d723a Mon Sep 17 00:00:00 2001 From: Sergey Fedoruk Date: Tue, 1 Jan 2019 23:58:59 +0100 Subject: [PATCH 08/10] add type annotations in run and create_server --- sanic/app.py | 90 ++++++++++++++++++++++++++++++++++--------------- sanic/config.py | 7 ++-- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index eb35e9ff..46ba8cd5 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -2,13 +2,15 @@ import logging import logging.config import os import re +import typing import warnings -from asyncio import CancelledError, ensure_future, get_event_loop +from asyncio import CancelledError, ensure_future, get_event_loop, Protocol from collections import defaultdict, deque from functools import partial from inspect import getmodulename, isawaitable, signature, stack -from ssl import Purpose, create_default_context +from socket import socket +from ssl import Purpose, create_default_context, SSLContext from traceback import format_exc from urllib.parse import urlencode, urlunparse @@ -967,34 +969,46 @@ class Sanic: def run( self, - host=None, - port=None, - debug=False, - ssl=None, - sock=None, - workers=1, - protocol=None, - backlog=100, - stop_event=None, - register_sys_signals=True, - access_log=None, - **kwargs - ): + host: typing.Optional[str]=None, + port: typing.Optional[int]=None, + debug: bool=False, + ssl: typing.Union[dict, SSLContext, None]=None, + sock: typing.Optional[socket]=None, + workers: int=1, + protocol: typing.Type[Protocol]=None, + backlog: int=100, + stop_event: typing.Any=None, + register_sys_signals: bool=True, + access_log: bool=None, + **kwargs: typing.Any + ) -> None: """Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing. :param host: Address to host on + :type host: str :param port: Port to host on + :type port: int :param debug: Enables debug output (slows server) + :type debug: bool :param ssl: SSLContext, or location of certificate and key for SSL encryption of worker(s) + :type ssl:SSLContext or dict :param sock: Socket for the server to accept connections from + :type sock: socket :param workers: Number of processes received before it is respected + :type workers: int + :param protocol: Subclass of asyncio Protocol class + :type protocol: type[Protocol] :param backlog: a number of unaccepted connections that the system will allow before refusing new connections - :param stop_event: event to be triggered before stopping the app + :type backlog: int + :param stop_event: event to be triggered before stopping the app - deprecated + :type stop_event: None :param register_sys_signals: Register SIG* events - :param protocol: Subclass of asyncio protocol class + :type register_sys_signals: bool + :param access_log: Enables writing access logs (slows server) + :type access_log: bool :return: Nothing """ if "loop" in kwargs: @@ -1085,16 +1099,16 @@ class Sanic: async def create_server( self, - host=None, - port=None, - debug=False, - ssl=None, - sock=None, - protocol=None, - backlog=100, - stop_event=None, - access_log=None, - ): + host: typing.Optional[str] = None, + port: typing.Optional[int] = None, + debug: bool = False, + ssl: typing.Union[dict, SSLContext, None] = None, + sock: typing.Optional[socket] = None, + protocol: typing.Type[Protocol] = None, + backlog: int = 100, + stop_event: typing.Any = None, + access_log: bool = None, + ) -> None: """ Asynchronous version of :func:`run`. @@ -1105,6 +1119,28 @@ class Sanic: .. note:: This does not support multiprocessing and is not the preferred way to run a :class:`Sanic` application. + + :param host: Address to host on + :type host: str + :param port: Port to host on + :type port: int + :param debug: Enables debug output (slows server) + :type debug: bool + :param ssl: SSLContext, or location of certificate and key + for SSL encryption of worker(s) + :type ssl:SSLContext or dict + :param sock: Socket for the server to accept connections from + :type sock: socket + :param protocol: Subclass of asyncio Protocol class + :type protocol: type[Protocol] + :param backlog: a number of unaccepted connections that the system + will allow before refusing new connections + :type backlog: int + :param stop_event: event to be triggered before stopping the app - deprecated + :type stop_event: None + :param access_log: Enables writing access logs (slows server) + :type access_log: bool + :return: Nothing """ if sock is None: diff --git a/sanic/config.py b/sanic/config.py index 5214f8f5..f1c6c26f 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,5 +1,6 @@ import os import types +from distutils.util import strtobool from sanic.exceptions import PyFileError @@ -124,7 +125,7 @@ class Config(dict): try: self[config_key] = float(v) except ValueError: - if v in ["True", "False"]: - self[config_key] = v == "True" - else: + try: + self[config_key] = bool(strtobool(v)) + except ValueError: self[config_key] = v From 65daaaf64bf08bc06cc7167b35f48a2ea639c431 Mon Sep 17 00:00:00 2001 From: Sergey Fedoruk Date: Wed, 2 Jan 2019 01:35:25 +0100 Subject: [PATCH 09/10] linteger fix and delete old tests --- sanic/app.py | 51 +++++++++++++++++--------------------------- sanic/config.py | 1 + tests/test_config.py | 10 +-------- 3 files changed, 22 insertions(+), 40 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 46ba8cd5..dfe047e9 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -5,12 +5,12 @@ import re import typing import warnings -from asyncio import CancelledError, ensure_future, get_event_loop, Protocol +from asyncio import CancelledError, Protocol, ensure_future, get_event_loop from collections import defaultdict, deque from functools import partial from inspect import getmodulename, isawaitable, signature, stack from socket import socket -from ssl import Purpose, create_default_context, SSLContext +from ssl import Purpose, SSLContext, create_default_context from traceback import format_exc from urllib.parse import urlencode, urlunparse @@ -969,17 +969,17 @@ class Sanic: def run( self, - host: typing.Optional[str]=None, - port: typing.Optional[int]=None, - debug: bool=False, - ssl: typing.Union[dict, SSLContext, None]=None, - sock: typing.Optional[socket]=None, - workers: int=1, - protocol: typing.Type[Protocol]=None, - backlog: int=100, - stop_event: typing.Any=None, - register_sys_signals: bool=True, - access_log: bool=None, + host: typing.Optional[str] = None, + port: typing.Optional[int] = None, + debug: bool = False, + ssl: typing.Union[dict, SSLContext, None] = None, + sock: typing.Optional[socket] = None, + workers: int = 1, + protocol: typing.Type[Protocol] = None, + backlog: int = 100, + stop_event: typing.Any = None, + register_sys_signals: bool = True, + access_log: typing.Optional[bool] = None, **kwargs: typing.Any ) -> None: """Run the HTTP Server and listen until keyboard interrupt or term @@ -1003,7 +1003,8 @@ class Sanic: :param backlog: a number of unaccepted connections that the system will allow before refusing new connections :type backlog: int - :param stop_event: event to be triggered before stopping the app - deprecated + :param stop_event: event to be triggered + before stopping the app - deprecated :type stop_event: None :param register_sys_signals: Register SIG* events :type register_sys_signals: bool @@ -1043,12 +1044,7 @@ class Sanic: ) # if access_log is passed explicitly change config.ACCESS_LOG if access_log is not None: - if isinstance(access_log, bool): - self.config.ACCESS_LOG = access_log - else: - raise ServerError( - ("'access_log' passed in 'run' should be boolean") - ) + self.config.ACCESS_LOG = access_log server_settings = self._helper( host=host, @@ -1107,7 +1103,7 @@ class Sanic: protocol: typing.Type[Protocol] = None, backlog: int = 100, stop_event: typing.Any = None, - access_log: bool = None, + access_log: typing.Optional[bool] = None, ) -> None: """ Asynchronous version of :func:`run`. @@ -1136,7 +1132,8 @@ class Sanic: :param backlog: a number of unaccepted connections that the system will allow before refusing new connections :type backlog: int - :param stop_event: event to be triggered before stopping the app - deprecated + :param stop_event: event to be triggered + before stopping the app - deprecated :type stop_event: None :param access_log: Enables writing access logs (slows server) :type access_log: bool @@ -1159,15 +1156,7 @@ class Sanic: ) # if access_log is passed explicitly change config.ACCESS_LOG if access_log is not None: - if isinstance(access_log, bool): - self.config.ACCESS_LOG = access_log - else: - raise ServerError( - ( - "'access_log' passed in 'create_server' " - "should be boolean" - ) - ) + self.config.ACCESS_LOG = access_log server_settings = self._helper( host=host, diff --git a/sanic/config.py b/sanic/config.py index f1c6c26f..a7183d77 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,5 +1,6 @@ import os import types + from distutils.util import strtobool from sanic.exceptions import PyFileError diff --git a/tests/test_config.py b/tests/test_config.py index 575800e3..fe4b97b9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,7 +7,7 @@ import pytest from sanic import Sanic from sanic.config import Config, DEFAULT_CONFIG -from sanic.exceptions import PyFileError, ServerError +from sanic.exceptions import PyFileError @contextmanager @@ -223,10 +223,6 @@ def test_config_access_log_passing_in_run(app): app.run(port=1340, access_log=True) assert app.config.ACCESS_LOG == True - with pytest.raises(ServerError) as e: - app.run(port=1340, access_log='string') - assert str(e.value) == ("'access_log' passed in 'run' should be boolean") - async def test_config_access_log_passing_in_create_server(app): assert app.config.ACCESS_LOG == True @@ -241,10 +237,6 @@ async def test_config_access_log_passing_in_create_server(app): await app.create_server(port=1342, access_log=True) assert app.config.ACCESS_LOG == True - with pytest.raises(ServerError) as e: - await app.create_server(port=1343, access_log='somestring') - assert str(e.value) == ("'access_log' passed in 'create_server' should be boolean") - def test_config_rewrite_keep_alive(): config = Config() From 102e651741e89a5fe925406253b60883f569a94c Mon Sep 17 00:00:00 2001 From: Sergey Fedoruk Date: Wed, 2 Jan 2019 23:24:58 +0100 Subject: [PATCH 10/10] refactor typing imports --- sanic/app.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index dfe047e9..244f4b68 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -2,7 +2,6 @@ import logging import logging.config import os import re -import typing import warnings from asyncio import CancelledError, Protocol, ensure_future, get_event_loop @@ -12,6 +11,7 @@ from inspect import getmodulename, isawaitable, signature, stack from socket import socket from ssl import Purpose, SSLContext, create_default_context from traceback import format_exc +from typing import Any, Optional, Type, Union from urllib.parse import urlencode, urlunparse from sanic import reloader_helpers @@ -969,18 +969,18 @@ class Sanic: def run( self, - host: typing.Optional[str] = None, - port: typing.Optional[int] = None, + host: Optional[str] = None, + port: Optional[int] = None, debug: bool = False, - ssl: typing.Union[dict, SSLContext, None] = None, - sock: typing.Optional[socket] = None, + ssl: Union[dict, SSLContext, None] = None, + sock: Optional[socket] = None, workers: int = 1, - protocol: typing.Type[Protocol] = None, + protocol: Type[Protocol] = None, backlog: int = 100, - stop_event: typing.Any = None, + stop_event: Any = None, register_sys_signals: bool = True, - access_log: typing.Optional[bool] = None, - **kwargs: typing.Any + access_log: Optional[bool] = None, + **kwargs: Any ) -> None: """Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing. @@ -1095,15 +1095,15 @@ class Sanic: async def create_server( self, - host: typing.Optional[str] = None, - port: typing.Optional[int] = None, + host: Optional[str] = None, + port: Optional[int] = None, debug: bool = False, - ssl: typing.Union[dict, SSLContext, None] = None, - sock: typing.Optional[socket] = None, - protocol: typing.Type[Protocol] = None, + ssl: Union[dict, SSLContext, None] = None, + sock: Optional[socket] = None, + protocol: Type[Protocol] = None, backlog: int = 100, - stop_event: typing.Any = None, - access_log: typing.Optional[bool] = None, + stop_event: Any = None, + access_log: Optional[bool] = None, ) -> None: """ Asynchronous version of :func:`run`.