8673021ad4
* Allow non-conforming ErrorHandlers (#2259) * Allow non-conforming ErrorHandlers * Rename to legacy lookup * Updated depnotice * Bump version * Fix formatting * Remove unused import * Fix error messages * Add error format commit and merge conflicts * Make HTTP connections start in IDLE stage, avoiding delays and error messages (#2268) * Make all new connections start in IDLE stage, and switch to REQUEST stage only once any bytes are received from client. This makes new connections without any request obey keepalive timeout rather than request timeout like they currently do. * Revert typo * Remove request timeout endpoint test which is no longer working (still tested by mocking). Fix mock timeout test setup. Co-authored-by: L. Karkkainen <tronic@users.noreply.github.com> * Bump version * Add error format from config replacement objects * Cleanup mistaken print statement * Cleanup reversions * Bump version Co-authored-by: L. Kärkkäinen <98187+Tronic@users.noreply.github.com> Co-authored-by: L. Karkkainen <tronic@users.noreply.github.com>
391 lines
10 KiB
Python
391 lines
10 KiB
Python
from contextlib import contextmanager
|
|
from os import environ
|
|
from pathlib import Path
|
|
from tempfile import TemporaryDirectory
|
|
from textwrap import dedent
|
|
from unittest.mock import Mock
|
|
|
|
import pytest
|
|
|
|
from sanic import Sanic
|
|
from sanic.config import DEFAULT_CONFIG, Config
|
|
from sanic.exceptions import PyFileError
|
|
|
|
|
|
@contextmanager
|
|
def temp_path():
|
|
"""a simple cross platform replacement for NamedTemporaryFile"""
|
|
with TemporaryDirectory() as td:
|
|
yield Path(td, "file")
|
|
|
|
|
|
class ConfigTest:
|
|
not_for_config = "should not be used"
|
|
CONFIG_VALUE = "should be used"
|
|
|
|
@property
|
|
def ANOTHER_VALUE(self):
|
|
return self.CONFIG_VALUE
|
|
|
|
@property
|
|
def another_not_for_config(self):
|
|
return self.not_for_config
|
|
|
|
|
|
def test_load_from_object(app):
|
|
app.config.load(ConfigTest)
|
|
assert "CONFIG_VALUE" in app.config
|
|
assert app.config.CONFIG_VALUE == "should be used"
|
|
assert "not_for_config" not in app.config
|
|
|
|
|
|
def test_load_from_object_string(app):
|
|
app.config.load("test_config.ConfigTest")
|
|
assert "CONFIG_VALUE" in app.config
|
|
assert app.config.CONFIG_VALUE == "should be used"
|
|
assert "not_for_config" not in app.config
|
|
|
|
|
|
def test_load_from_instance(app):
|
|
app.config.load(ConfigTest())
|
|
assert "CONFIG_VALUE" in app.config
|
|
assert app.config.CONFIG_VALUE == "should be used"
|
|
assert app.config.ANOTHER_VALUE == "should be used"
|
|
assert "not_for_config" not in app.config
|
|
assert "another_not_for_config" not in app.config
|
|
|
|
|
|
def test_load_from_object_string_exception(app):
|
|
with pytest.raises(ImportError):
|
|
app.config.load("test_config.Config.test")
|
|
|
|
|
|
def test_auto_env_prefix():
|
|
environ["SANIC_TEST_ANSWER"] = "42"
|
|
app = Sanic(name=__name__)
|
|
assert app.config.TEST_ANSWER == 42
|
|
del environ["SANIC_TEST_ANSWER"]
|
|
|
|
|
|
def test_auto_bool_env_prefix():
|
|
environ["SANIC_TEST_ANSWER"] = "True"
|
|
app = Sanic(name=__name__)
|
|
assert app.config.TEST_ANSWER is True
|
|
del environ["SANIC_TEST_ANSWER"]
|
|
|
|
|
|
def test_dont_load_env():
|
|
environ["SANIC_TEST_ANSWER"] = "42"
|
|
app = Sanic(name=__name__, load_env=False)
|
|
assert getattr(app.config, "TEST_ANSWER", None) is None
|
|
del environ["SANIC_TEST_ANSWER"]
|
|
|
|
|
|
@pytest.mark.parametrize("load_env", [None, False, "", "MYAPP_"])
|
|
def test_load_env_deprecation(load_env):
|
|
with pytest.warns(DeprecationWarning, match=r"21\.12"):
|
|
_ = Sanic(name=__name__, load_env=load_env)
|
|
|
|
|
|
def test_load_env_prefix():
|
|
environ["MYAPP_TEST_ANSWER"] = "42"
|
|
app = Sanic(name=__name__, load_env="MYAPP_")
|
|
assert app.config.TEST_ANSWER == 42
|
|
del environ["MYAPP_TEST_ANSWER"]
|
|
|
|
|
|
@pytest.mark.parametrize("env_prefix", [None, ""])
|
|
def test_empty_load_env_prefix(env_prefix):
|
|
environ["SANIC_TEST_ANSWER"] = "42"
|
|
app = Sanic(name=__name__, env_prefix=env_prefix)
|
|
assert getattr(app.config, "TEST_ANSWER", None) is None
|
|
del environ["SANIC_TEST_ANSWER"]
|
|
|
|
|
|
def test_load_env_prefix_float_values():
|
|
environ["MYAPP_TEST_ROI"] = "2.3"
|
|
app = Sanic(name=__name__, load_env="MYAPP_")
|
|
assert app.config.TEST_ROI == 2.3
|
|
del environ["MYAPP_TEST_ROI"]
|
|
|
|
|
|
def test_load_env_prefix_string_value():
|
|
environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken"
|
|
app = Sanic(name=__name__, load_env="MYAPP_")
|
|
assert app.config.TEST_TOKEN == "somerandomtesttoken"
|
|
del environ["MYAPP_TEST_TOKEN"]
|
|
|
|
|
|
def test_env_prefix():
|
|
environ["MYAPP_TEST_ANSWER"] = "42"
|
|
app = Sanic(name=__name__, env_prefix="MYAPP_")
|
|
assert app.config.TEST_ANSWER == 42
|
|
del environ["MYAPP_TEST_ANSWER"]
|
|
|
|
|
|
def test_env_prefix_float_values():
|
|
environ["MYAPP_TEST_ROI"] = "2.3"
|
|
app = Sanic(name=__name__, env_prefix="MYAPP_")
|
|
assert app.config.TEST_ROI == 2.3
|
|
del environ["MYAPP_TEST_ROI"]
|
|
|
|
|
|
def test_env_prefix_string_value():
|
|
environ["MYAPP_TEST_TOKEN"] = "somerandomtesttoken"
|
|
app = Sanic(name=__name__, env_prefix="MYAPP_")
|
|
assert app.config.TEST_TOKEN == "somerandomtesttoken"
|
|
del environ["MYAPP_TEST_TOKEN"]
|
|
|
|
|
|
def test_load_from_file(app):
|
|
config = dedent(
|
|
"""
|
|
VALUE = 'some value'
|
|
condition = 1 == 1
|
|
if condition:
|
|
CONDITIONAL = 'should be set'
|
|
"""
|
|
)
|
|
with temp_path() as config_path:
|
|
config_path.write_text(config)
|
|
app.config.load(str(config_path))
|
|
assert "VALUE" in app.config
|
|
assert app.config.VALUE == "some value"
|
|
assert "CONDITIONAL" in app.config
|
|
assert app.config.CONDITIONAL == "should be set"
|
|
assert "condition" not in app.config
|
|
|
|
|
|
def test_load_from_missing_file(app):
|
|
with pytest.raises(IOError):
|
|
app.config.load("non-existent file")
|
|
|
|
|
|
def test_load_from_envvar(app):
|
|
config = "VALUE = 'some value'"
|
|
with temp_path() as config_path:
|
|
config_path.write_text(config)
|
|
environ["APP_CONFIG"] = str(config_path)
|
|
app.config.load("${APP_CONFIG}")
|
|
assert "VALUE" in app.config
|
|
assert app.config.VALUE == "some value"
|
|
|
|
|
|
def test_load_from_missing_envvar(app):
|
|
with pytest.raises(IOError) as e:
|
|
app.config.load("non-existent variable")
|
|
assert str(e.value) == (
|
|
"The environment variable 'non-existent "
|
|
"variable' is not set and thus configuration "
|
|
"could not be loaded."
|
|
)
|
|
|
|
|
|
def test_load_config_from_file_invalid_syntax(app):
|
|
config = "VALUE = some value"
|
|
with temp_path() as config_path:
|
|
config_path.write_text(config)
|
|
|
|
with pytest.raises(PyFileError):
|
|
app.config.load(config_path)
|
|
|
|
|
|
def test_overwrite_exisiting_config(app):
|
|
app.config.DEFAULT = 1
|
|
|
|
class Config:
|
|
DEFAULT = 2
|
|
|
|
app.config.load(Config)
|
|
assert app.config.DEFAULT == 2
|
|
|
|
|
|
def test_overwrite_exisiting_config_ignore_lowercase(app):
|
|
app.config.default = 1
|
|
|
|
class Config:
|
|
default = 2
|
|
|
|
app.config.load(Config)
|
|
assert app.config.default == 1
|
|
|
|
|
|
def test_missing_config(app):
|
|
with pytest.raises(AttributeError, match="Config has no 'NON_EXISTENT'"):
|
|
_ = app.config.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]
|
|
|
|
|
|
def test_config_access_log_passing_in_run(app):
|
|
assert app.config.ACCESS_LOG is 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 is False
|
|
|
|
app.run(port=1340, access_log=True)
|
|
assert app.config.ACCESS_LOG is True
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_config_access_log_passing_in_create_server(app):
|
|
assert app.config.ACCESS_LOG is True
|
|
|
|
@app.listener("after_server_start")
|
|
async def _request(sanic, loop):
|
|
app.stop()
|
|
|
|
await app.create_server(
|
|
port=1341, access_log=False, return_asyncio_server=True
|
|
)
|
|
assert app.config.ACCESS_LOG is False
|
|
|
|
await app.create_server(
|
|
port=1342, access_log=True, return_asyncio_server=True
|
|
)
|
|
assert app.config.ACCESS_LOG is 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 is True
|
|
config = Config(keep_alive=False)
|
|
assert config.KEEP_ALIVE is False
|
|
|
|
# use defaults
|
|
config = Config(defaults={"KEEP_ALIVE": False})
|
|
assert config.KEEP_ALIVE is False
|
|
config = Config(defaults={"KEEP_ALIVE": True})
|
|
assert config.KEEP_ALIVE is True
|
|
|
|
|
|
_test_setting_as_dict = {"TEST_SETTING_VALUE": 1}
|
|
_test_setting_as_class = type("C", (), {"TEST_SETTING_VALUE": 1})
|
|
_test_setting_as_module = str(
|
|
Path(__file__).parent / "static/app_test_config.py"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"conf_object",
|
|
[
|
|
_test_setting_as_dict,
|
|
_test_setting_as_class,
|
|
_test_setting_as_module,
|
|
],
|
|
ids=["from_dict", "from_class", "from_file"],
|
|
)
|
|
def test_update(app, conf_object):
|
|
app.update_config(conf_object)
|
|
assert app.config["TEST_SETTING_VALUE"] == 1
|
|
|
|
|
|
def test_update_from_lowercase_key(app):
|
|
d = {"test_setting_value": 1}
|
|
app.update_config(d)
|
|
assert "test_setting_value" not in app.config
|
|
|
|
|
|
def test_deprecation_notice_when_setting_logo(app):
|
|
message = (
|
|
"Setting the config.LOGO is deprecated and will no longer be "
|
|
"supported starting in v22.6."
|
|
)
|
|
with pytest.warns(DeprecationWarning, match=message):
|
|
app.config.LOGO = "My Custom Logo"
|
|
|
|
|
|
def test_config_set_methods(app, monkeypatch):
|
|
post_set = Mock()
|
|
monkeypatch.setattr(Config, "_post_set", post_set)
|
|
|
|
app.config.FOO = 1
|
|
post_set.assert_called_once_with("FOO", 1)
|
|
post_set.reset_mock()
|
|
|
|
app.config["FOO"] = 2
|
|
post_set.assert_called_once_with("FOO", 2)
|
|
post_set.reset_mock()
|
|
|
|
app.config.update({"FOO": 3})
|
|
post_set.assert_called_once_with("FOO", 3)
|
|
post_set.reset_mock()
|
|
|
|
app.config.update([("FOO", 4)])
|
|
post_set.assert_called_once_with("FOO", 4)
|
|
post_set.reset_mock()
|
|
|
|
app.config.update(FOO=5)
|
|
post_set.assert_called_once_with("FOO", 5)
|
|
post_set.reset_mock()
|
|
|
|
app.config.update_config({"FOO": 6})
|
|
post_set.assert_called_once_with("FOO", 6)
|