Конфиги. Все хранят их по разному. Кто-то в .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
, но я считаю, что он слишком НЕлегковесный для таких задач.