Конфиги. Все хранят их по разному. Кто-то в .yaml, кто-то в .ini, а кто-то вообще в исходном коде, подумав, что "Путь Django" с его settings.py действительно хорош.


В этой статье, я хочу попробовать найти идеальный (вероятнее всего) способ хранения и использования конфигурационных файлов в Python. Ну, а также поделиться своей библиотекой для них :)


Попытка №1


А что насчёт того чтобы хранить конфигурацию в коде? Ну, а что, вроде удобно, да и новых языков не придётся изучать. Существует множество проектов, в которых данный способ используется, и хочу сказать, вполне успешно.


Типичный конфиг в этом стиле выглядит так:


# settings.py

TWITTER_USERNAME="johndoe"
TWITTER_PASSWORD="johndoespassword"
TWITTER_TOKEN="......."

Выглядит неплохо. Только одно настораживает, почему секьюрные данные хранятся в коде? Как мы это коммитить будем? Загадка. Разве что вносить наш файл в .gitignore, но это, конечно, вообще не решение.


Да и вообще, почему хоть какие-то данные хранятся в коде? Как мне кажется код, он на то и код, что должен выполнять какую-то логику, а не хранить данные.


Данный подход, на самом деле используется много где. В том же Django. Все думают, что раз это самый популярный фреймворк, который используется в самом Инстаграме, то они то уж плохое советовать не будут. Жаль, что это не так.


Чуть более подробно об этом.


Попытка №2


Ладно, раз уж мы решили, что хранить данные в коде — не круто, то давайте искать альтернативу. Для конфигурационных файлов изобретено немалое количество различных форматов, в последнее время набирают большую популярность toml.


Но мы начнём с того, что нам предлагает сам Python — .ini. В стандартной библиотеке имеется библиотека configparser.


Наш конфиг, который мы уже писали ранее:


# settings.ini
[Twitter]
username="johndoe"
password="johndoespassword"
token="....."

А теперь прочитаем в Python:


import configparser  # импортируем библиотеку

config = configparser.ConfigParser()  # создаём объекта парсера
config.read("settings.ini")  # читаем конфиг

print(config["Twitter"]["username"])  # обращаемся как к обычному словарю!
# 'johndoe'

Все проблемы решены. Данные хранятся не в коде, доступ прост. Но… а если нам нужно читать другие конфиги, ну там json или yaml например, или все сразу. Конечно, есть json в стандартной библиотеке и pyyaml, но придётся написать кучу (ну, или не совсем) кода для этого.


Документация.


Попытка №3


А сейчас, я хотел бы показать Вам свою библиотеку, которая призвана решить все эти проблемы (ну, или хотя бы уменьшить ваши страдания :)).


Называется она betterconf и доступна на PyPi.


Установка так же проста, как и любой другой библиотеки:


pip install betterconf

Изначально, наш конфиг представлен в виде класса с полями:


# settings.py
from betterconf import Config, field

class TwitterConfig(Config):  # объявляем класс, который наследуется от `Config`
    username = field("TWITTER_USERNAME", default="johndoe")  # объявляем поле `username`, если оно не найдено, выставляем стандартное
    password = field("TWITTER_PASSWORD", default="johndoespassword") # аналогично
    token = field("TWITTER_TOKEN", default=lambda: raise RuntimeError("Account's token must be defined!")  # делаем тоже самое, но при отсутствии токенавозбуждаем ошибку

cfg = TwitterConfig()
print(cfg.username)
# 'johndoe'

По умолчанию, библиотека пытается взять значения из переменных окружения, но мы также можем настроить и это:


from betterconf import Config, field
from betterconf.config import AbstractProvider

import json

class JSONProvider(AbstractProvider):  # наследуемся от абстрактного класса
    SETTINGS_JSON_FILE = "settings.json"  # путь до файла с настройками

    def __init__(self):
        with open(self.SETTINGS_JSON_FILE, "r") as f:
            self._settings = json.load(f)  # открываем и читаем

    def get(self, name):
        return self._settings.get(name)  # если значение есть - возвращаем его, иначе - None. Библиотека будет выбрасывать свою исключением, если получит None.

provider = JSONProvider()

class TwitterConfig(Config):
    username = field("twitter_username", provider=provider)  # используем наш способ получения данных
    # ...

cfg = TwitterConfig()
# ...

Из этого примера следует, что мы можем применять различные провайдеры для получения данных. И это действительно иногда бывает удобно, говорю из личного опыта.


Хорошо, а что если у нас в конфигах есть булевые значения, или числа, они же в итоге будут все равно приходить в строках. И для этого есть решение:


from betterconf import Config, field
# из коробки доступно всего 2 кастера
from betterconf.caster import to_bool, to_int

class TwitterConfig(Config):
    # ...
    post_tweets = field("TWITTER_POST_TWEETS", caster=to_bool)

# ...

Таким образом, все похожие на булевые типы значения (а именно true и false будут преобразованы в питоновский bool. Регистр не учитывается.


Свой кастер написать также легко:


from betterconf.caster import AbstractCaster

class DashToDotCaster(AbstractCaster):
    def cast(self, val):
        return val.replace("-", ".")  # заменяет тире на точки

to_dot = DashToDotCaster() 
# ...

Репозиторий на Github с более подробной документацией.


Итоги


Таким образом, мы пришли к выводу, что хранить настройки в исходных кодах — не есть хорошо. Для этого уже придуманы различные форматы. Ну, а вы познакомились с ещё одной полезной (как я считаю :)) библиотекой.


P.S


Да, также можно было включить и Pydantic, но я считаю, что он слишком НЕлегковесный для таких задач.