Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Env custom type casting #2330

Merged
merged 19 commits into from Dec 20, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
37 changes: 35 additions & 2 deletions sanic/config.py
@@ -1,12 +1,22 @@
from __future__ import annotations

from collections import deque
from inspect import isclass
from os import environ
from pathlib import Path
from typing import TYPE_CHECKING, Any, Dict, Optional, Union
from typing import (
TYPE_CHECKING,
Any,
Callable,
Dict,
Optional,
Sequence,
Union,
)
from warnings import warn

from sanic.errorpages import check_error_format
from sanic.exceptions import SanicException
from sanic.http import Http
from sanic.utils import load_module_from_file_location, str_to_bool

Expand Down Expand Up @@ -45,8 +55,23 @@
"WEBSOCKET_PING_TIMEOUT": 20,
}

# These values will be removed from the Config object in v22.6 and moved
# to the application state
DEPRECATED_CONFIG = ("SERVER_RUNNING", "RELOADER_PROCESS", "RELOADED_FILES")


class CastRegistry(deque):
def add(self, cast: Callable[[str], Any]) -> None:
if cast in self:
raise SanicException(
f"Type {cast.__name__} has already been registered"
)
ahopkins marked this conversation as resolved.
Show resolved Hide resolved
self.appendleft(cast)


class Config(dict):
__registry__ = CastRegistry((int, float, str_to_bool, str))

ACCESS_LOG: bool
AUTO_RELOAD: bool
EVENT_AUTOREGISTER: bool
Expand Down Expand Up @@ -81,13 +106,17 @@ def __init__(
keep_alive: Optional[bool] = None,
*,
app: Optional[Sanic] = None,
converters: Optional[Sequence[Callable[[str], Any]]] = None,
):
defaults = defaults or {}
super().__init__({**DEFAULT_CONFIG, **defaults})

self._app = app
self._LOGO = ""

if converters:
self.register_type(*converters)

if keep_alive is not None:
self.KEEP_ALIVE = keep_alive

Expand Down Expand Up @@ -192,7 +221,7 @@ def load_environment_vars(self, prefix=SANIC_PREFIX):

_, config_key = key.split(prefix, 1)

for converter in (int, float, str_to_bool, str):
for converter in self.__registry__:
ahopkins marked this conversation as resolved.
Show resolved Hide resolved
try:
self[config_key] = converter(value)
break
Expand Down Expand Up @@ -267,3 +296,7 @@ class C:
self.update(config)

load = update_config

def register_type(self, *cast: Callable[[str], Any]) -> None:
for item in cast:
self.__registry__.add(item)
15 changes: 15 additions & 0 deletions tests/test_config.py
Expand Up @@ -32,6 +32,11 @@ def another_not_for_config(self):
return self.not_for_config


class UltimateAnswer:
def __init__(self, answer):
self.answer = int(answer)


def test_load_from_object(app):
app.config.load(ConfigTest)
assert "CONFIG_VALUE" in app.config
Expand Down Expand Up @@ -137,6 +142,16 @@ def test_env_prefix_string_value():
del environ["MYAPP_TEST_TOKEN"]


def test_env_w_custom_converter():
environ["SANIC_TEST_ANSWER"] = "42"

config = Config(converters=[UltimateAnswer])
app = Sanic(name=__name__, config=config)
assert isinstance(app.config.TEST_ANSWER, UltimateAnswer)
assert app.config.TEST_ANSWER.answer == 42
del environ["SANIC_TEST_ANSWER"]


def test_load_from_file(app):
config = dedent(
"""
Expand Down