Словил себя на мысли, что несмотря на то, что большинство моих веб-приложений работают на aiohttp.web, их настройка происходит в лучших Django традициях,
- Определение настроек в
settings.py
модуле, какDEBUG = to_bool(os.getenv("DEBUG") or False)
- Импорт целого модуля и сохранение его в
aiohttp.web
приложении, какtypes.MappingTypeProxy
структура - Использование сохраненного маппинга во вью-функциях, как, например,
request.config_dict["settings"]["DEBUG"]
И если этот подход работал like a magic 5 лет назад, не говорит о том, что он идеально вписывается в Python в 2020 году. Главные проблемы с тем, как это было сделано ранее, это:
- Инстинктивно хочется сделать
from app import settings
и потом простоsettings.DEBUG
, вместо того чтоб идти за настройками вrequest.config_dict
- Работать с настройками, которые хранятся как
Mapping[str, Any]
— это выстрел себе в обе ноги. Никакойmypy
не подскажет тебе, есть ли у тебя в настройках ключVERY_IMPORTANT_KEY
и не ошибься ли ты с тем, что считаешь, чтоPRICE_MULTIPLICATOR
этоdecimal.Decimal
, а неfloat
.
Что же делать? Как обычно есть два пути, взять что-то готовое или написать свое. Второй путь, куда более заманчивый, поэтому план такой,
- Нужно хранить настройке в какой-то структуре данных, а что может быть лучше
@attr.dataclass
для этого? - Нужно упростить получение настроек из окружения для
attr.ib
(аттрибутов) структуры данных - Ну и куда же без
mypy
, так что нужно убедиться, что структура данных с настройками будет поддерживать любые типы данных и работать без магических# type: ignore
План рабочий, поэтому на его реализацию много времени не понадобилось. Начал с env_factory
:
import os
from typing import Optional, overload, TypeVar
import attr
from rororo.settings import to_bool
T = TypeVar("T")
@overload
def env_factory(name: str) -> Optional[str]:
...
@overload
def env_factory(name: str, default: T) -> T:
...
def env_factory(name: str, default: T = None) -> Union[Optional[str], T]:
def getenv() -> Union[Optional[str], T]:
value = os.getenv(name)
if default is None:
return value
if value is None:
return default
expected_type = type(default)
if isinstance(value, expected_type):
return value
try:
if expected_type is bool:
return to_bool(value) # type: ignore
return expected_type(value) # type: ignore
except (TypeError, ValueError):
raise ValueError(
f"Unable to convert {name} env var to {expected_type}."
)
return attr.Factory(getenv)
Продолжил имплементацией хранения базовых настроек,
import attr
from rororo.settings import setup_locale, setup_logging, setup_timezone
@attr.dataclass(frozen=True, slots=True)
class BaseSettings:
# Base aiohttp settings
host: str = env_factory("AIOHTTP_HOST", "localhost")
port: int = env_factory("AIOHTTP_PORT", 8080)
# Base application settings
debug: bool = env_factory("DEBUG", False)
level: Level = env_factory("LEVEL", "dev")
# Date & time settings
time_zone: str = env_factory("TIME_ZONE", "UTC")
# Locale settings
first_weekday: int = env_factory("FIRST_WEEKDAY", 0)
locale: str = env_factory("LOCALE", "en_US.UTF-8")
# Sentry settings
sentry_dsn: Optional[str] = env_factory("SENTRY_DSN")
sentry_release: Optional[str] = env_factory("SENTRY_RELEASE")
def apply(
self,
*,
loggers: Iterable[str] = None,
remove_root_handlers: bool = False,
) -> None:
if loggers:
setup_logging(
default_logging_dict(*loggers),
remove_root_handlers=remove_root_handlers,
)
setup_locale(self.locale, self.first_weekday)
setup_timezone(self.time_zone)
И в конце концов осталось только начать использовать это все в create_app
фабриках и во вью-функциях.
def create_app(argv: List[str] = None, **options: Any) -> web.Application:
settings = Settings()
settings.apply()
app = web.Application(...)
app["settings"] = settings
return app
async def index(request: web.Request) -> web.Response:
settings: Settings = request.config_dict["settings"] # For JEDI autocomplete
if settings.debug:
print("Hello, world!")
return web.json_response(True)
В итоге настройки становится проще инициализировать, переопределять, дебажить. А mypy
говорит отдельное спасибо и точно оберегает от любых погрешностей, работая с базовым функционалом приложения.
ps. Но в заключение хочется отметить, что жаль, что web.Application не ожидает от пользователя никаких настроек и поэтому каждому разработчику приходится ломать голову в поисках того или иного решения для такой тривиальной задачи.
pss. Если вы заметили, то env_factory
содержит два # type: ignore
комментария. Хотел бы объяснить зачем они там. Первый на линии с to_bool
вызовом из-за того, что mypy
не понимает, что T is bool
и поэтому выдает ошибку Incompatible return value type
. Ну а во втором случае mypy
ругается на Too many arguments for "object"
. Отакої