Конфиги. Все хранят их по разному. Кто-то в .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
, но я считаю, что он слишком НЕлегковесный для таких задач.
vilgeforce
Столько писанины ради хранения конфигов? Увольте! Вы забыли, кстати, про передачу параметров через командную строку
prostomarkeloff Автор
Как часто Вы передаёте конфиг приложения в виде аргументов командый строки? :) (А он, вероятно не маленький)
vilgeforce
Адрес и порт прокси? Имя файла со входными данными? Да постоянно!
prostomarkeloff Автор
Кажется, количество параметров в моих проектах чутка больше.
Antonto
Конфигурационный файл, переменные окружения, параметры коммандной строки — все это служит для настройки работы программы. И логично, обрабатывать это все совместно.
Taxopr
можете порекомендовать что-то?