В прикладной разработке параметры программы обычно не размещают непосредственно в исходном коде. Токены, адреса серверов, номера портов, режимы запуска, имена пользователей, пароли и иные значения конфигурационного характера выносятся во внешний файл настроек. Такой способ организации данных позволяет отделить служебные параметры от программной логики, упростить сопровождение проекта и уменьшить вероятность ошибок при изменении окружения.
Ниже рассматривается модуль Python, который читает файл settings.toml, преобразует его содержимое в структуры Python, извлекает нужный раздел конфигурации и проверяет его через модель Pydantic.
Исходный код:
from functools import lru_cachefrom pathlib import Pathfrom tomllib import loadfrom typing import Any, Type, TypeVarfrom pydantic import BaseModel, SecretStrConfigType = TypeVar("ConfigType", bound=BaseModel)class BotConfig(BaseModel): token: SecretStr@lru_cachedef parse_config_file() -> dict[str, Any]: config_path = Path(__file__).resolve().parent.parent.joinpath("settings.toml") if not config_path.exists(): error = "Could not find settings file" raise ValueError(error) with open(config_path, "rb") as file: config_data = load(file) return config_datadef get_config(model: Type[ConfigType], root_key: str) -> ConfigType: config_dict = parse_config_file() if root_key not in config_dict: error = f"Key {root_key} not found" raise ValueError(error) return model.model_validate(config_dict[root_key])
1. Конфигурация
Прежде чем разбирать код, необходимо установить, что именно понимается под конфигурацией программы.
Конфигурацией называют совокупность параметров, от которых зависит работа приложения, но которые не составляют его алгоритмическую сущность. Например, телеграм-боту требуется токен доступа к API. Веб-приложению требуется адрес базы данных и номер порта. Почтовому сервису необходимы имя SMTP-сервера, логин и пароль. Эти значения должны использоваться программой, но не должны быть зашиты в код.
Если конфигурационные данные записываются прямо в модуле Python, возникает несколько затруднений. Изменение одного параметра требует редактирования исходников. Секретные данные могут попасть в репозиторий. Переключение между рабочей, тестовой и локальной средой становится неудобным. Кроме того, при таком подходе код программы начинает содержать информацию, которая не относится к самой логике вычислений.
Именно поэтому конфигурацию выносят во внешний файл. Программа читает этот файл при запуске, получает оттуда нужные значения и строит на их основе внутренние объекты.
В рассматриваемом модуле решаются следующие задачи. Программа должна определить путь к файлу settings.toml, убедиться, что файл существует, открыть его в подходящем режиме, разобрать его содержимое, извлечь нужный раздел, проверить корректность структуры данных и вернуть результат в виде объекта модели. При этом повторное чтение файла должно быть исключено, если содержимое конфигурации уже было загружено ранее.
2. Формат TOML и его представление в Python
Теперь необходимо понять, что представляет собой файл settings.toml.
TOML — это текстовый формат конфигурационных файлов. Его основное достоинство заключается в том, что он достаточно прост для чтения человеком и в то же время достаточно строг для надёжного машинного разбора. В конфигурационных задачах это особенно важно.
Простейший файл может выглядеть так:
[bot]token = "123456:ABCDEF"
Строка [bot] обозначает раздел конфигурации. Внутри раздела задаётся параметр token, которому присвоено строковое значение.
После чтения этого файла Python не работает с текстом напрямую. Сначала содержимое интерпретируется библиотекой разбора TOML, и результатом становится словарь Python:
{ "bot": { "token": "123456:ABCDEF" }}
Следовательно, данные проходят три состояния. Изначально это текстовый файл. После разбора это словарь Python. После валидации через Pydantic это объект модели.
3. Общая архитектура модуля
В структурном отношении модуль состоит из нескольких логически связанных частей.
Сначала импортируются библиотеки и используемые объекты. Затем объявляется переменная типа ConfigType, которая ограничивается классом BaseModel. После этого определяется модель BotConfig, описывающая структуру секции [bot]. Далее следует функция parse_config_file(), читающая и разбирающая весь конфигурационный файл. Наконец, функция get_config() извлекает из общего словаря конкретный раздел и превращает его в объект нужной модели.
Такое построение является корректным по нескольким причинам. Чтение файла вынесено в отдельную функцию. Проверка структуры данных и создание модели вынесены в отдельную функцию более высокого уровня. Модель конфигурации существует независимо от механизма чтения файла. За счёт этого код сохраняет ясное разделение обязанностей.
4. Импортируемые библиотеки
4.1. Модуль functools и декоратор lru_cache
Первая строка:
from functools import lru_cache
Модуль functools содержит инструменты, предназначенные для работы с функциями. В данном коде используется декоратор lru_cache.
Чтобы понять его назначение, сначала необходимо объяснить саму идею декоратора.
Декоратор в Python — это специальный механизм, при помощи которого одна функция может изменить поведение другой функции. Синтаксически это выражается записью с символом @ перед объявлением функции.
Пример:
def my_decorator(func): def wrapper(): print("Перед вызовом функции") result = func() print("После вызова функции") return result return wrapper@my_decoratordef say_hello(): print("Hello")say_hello()
Результат выполнения:
Перед вызовом функцииHelloПосле вызова функции
Следовательно, декоратор позволяет добавить функции дополнительное поведение без изменения её основного тела.
lru_cache является готовым стандартным декоратором, добавляющим к функции кэширование результата. Кэширование означает, что результат первого вызова запоминается. Если функция впоследствии будет вызвана с теми же аргументами, Python не станет выполнять её тело заново, а вернёт уже сохранённое значение.
Пример:
from functools import lru_cache@lru_cachedef get_number() -> int: print("Функция выполняется") return 10print(get_number())print(get_number())print(get_number())
Результат:
Функция выполняется101010
Сообщение выводится только один раз, потому что выполнение функции происходит лишь при первом обращении.
В конфигурационном модуле такое поведение особенно полезно. Файл настроек обычно читается один раз при запуске программы. Повторное чтение того же самого файла является избыточной операцией. Поэтому результат функции чтения конфигурации целесообразно сохранить в памяти и возвращать его повторно без повторного обращения к диску.
Необходимо учитывать и обратную сторону этого механизма. Если файл settings.toml был прочитан, а затем изменён на диске во время работы программы, функция, декорированная lru_cache, продолжит возвращать старое значение из кэша. Если требуется перечитать файл, кэш необходимо очистить явно.
Пример очистки:
parse_config_file.cache_clear()
После этого следующий вызов снова выполнит чтение файла.
4.2. Модуль pathlib и класс Path
Следующая строка импорта:
from pathlib import Path
Модуль pathlib предназначен для работы с файловыми путями. В более старых вариантах кода пути часто собирались вручную как строки, однако такой подход неудобен и легко приводит к ошибкам. Класс Path предоставляет объектное представление пути и набор методов для его обработки.
Пример:
from pathlib import Pathpath = Path("config").joinpath("settings.toml")print(path)
Результат:
config/settings.toml
На различных операционных системах внешний вид разделителей может отличаться, однако логика остаётся одинаковой: создаётся путь к файлу settings.toml внутри каталога config.
У объекта Path имеются методы и свойства, которые активно применяются в конфигурационных задачах. Например, метод .exists() проверяет существование пути, а свойство .parent позволяет перейти к родительскому каталогу.
Пример:
from pathlib import Pathfile_path = Path("settings.toml")print(file_path.exists())print(file_path.parent)
Если файл существует в текущем каталоге, первый вывод будет True. Если не существует, будет False. Второй вывод покажет родительский каталог пути.
Следовательно, Path необходим здесь для безопасного и ясного построения абсолютного пути к конфигурационному файлу.
4.3. Модуль tomllib и функция load
Следующая строка:
from tomllib import load
Модуль tomllib служит для чтения данных формата TOML. Он входит в стандартную библиотеку Python, начиная с версии 3.11. Если используется более ранняя версия Python, потребуется внешняя библиотека tomli.
Функция load() принимает уже открытый файловый объект и возвращает словарь Python, соответствующий содержимому файла.
Пример. Пусть существует файл settings.toml со следующим содержимым:
[bot]token = "abc123"
Тогда код:
from tomllib import loadwith open("settings.toml", "rb") as file: data = load(file)print(data)
даст результат:
{'bot': {'token': 'abc123'}}
Здесь особенно важно то, что файл открывается в режиме "rb", то есть в бинарном режиме чтения. Символ r обозначает чтение, а символ b обозначает бинарный режим. Для функции tomllib.load() это техническое требование интерфейса.
4.4. Модуль typing, объекты Type, TypeVar и Any
Следующая группа импортов:
from typing import Any, Type, TypeVar
Все эти объекты относятся к системе аннотаций типов.
Аннотации типов делают код понятнее, помогают средствам статического анализа, улучшают подсказки редактора и точнее описывают намерение автора.
Объект Any используется тогда, когда значение может иметь произвольный тип. В функции чтения TOML это оправдано, поскольку после разбора конфигурационный словарь может содержать строки, числа, логические значения, вложенные словари и другие структуры.
Объект Type применяется в тех случаях, когда в параметр функции передаётся не экземпляр класса, а сам класс.
Пример:
from typing import Typeclass User: passdef create_instance(cls: Type[User]) -> User: return cls()user = create_instance(User)print(type(user))
Результат:
<class '__main__.User'>
Здесь параметр cls — это класс User, а не объект User().
Объект TypeVar используется для объявления переменной типа. Он нужен в тех случаях, когда функция является универсальной и должна возвращать результат того же типа, который был передан в виде класса.
Пример:
from typing import Type, TypeVarT = TypeVar("T")def create_instance(cls: Type[T]) -> T: return cls()
Если в эту функцию передать класс User, результат будет рассматриваться как User. Если передать другой класс, результат будет иметь тип этого другого класса.
Именно этот механизм применяется далее при описании универсальной функции получения конфигурации.
4.5. Библиотека Pydantic, классы BaseModel и SecretStr
Последний импорт:
from pydantic import BaseModel, SecretStr
Pydantic — это библиотека для проверки и преобразования структурированных данных. Она особенно полезна в тех случаях, когда программа получает данные из внешнего источника: из файла, из HTTP-запроса, из базы данных, из переменных окружения или из конфигурации.
Центральной сущностью в Pydantic является BaseModel. Пользовательские модели наследуются от него и описывают ожидаемую структуру данных.
Пример:
from pydantic import BaseModelclass DbConfig(BaseModel): host: str port: int
Такая модель означает, что объект конфигурации базы данных должен содержать поле host типа str и поле port типа int.
Создание объекта модели на основе словаря выполняется через метод model_validate():
from pydantic import BaseModelclass DbConfig(BaseModel): host: str port: intdata = {"host": "localhost", "port": 5432}config = DbConfig.model_validate(data)print(config)print(config.host)print(config.port)
Результат:
host='localhost' port=5432localhost5432
Если структура не соответствует модели, возникает исключение валидации.
Тип SecretStr предназначен для хранения строк, содержащих конфиденциальные данные. Если использовать обычный тип str, то при печати модели секретное значение будет отображаться открыто. Если использовать SecretStr, то при выводе на экран или в логах значение будет маскироваться.
Пример:
from pydantic import BaseModel, SecretStrclass BotConfig(BaseModel): token: SecretStrconfig = BotConfig.model_validate({"token": "super_secret_token"})print(config)print(config.token)print(config.token.get_secret_value())
Результат:
token=SecretStr('**********')**********super_secret_token
Следовательно, реальное значение сохраняется, но его случайный вывод затрудняется.
5. Объявление переменной типа ConfigType
Теперь можно перейти к строке:
ConfigType = TypeVar("ConfigType", bound=BaseModel)
Эта запись объявляет переменную типа ConfigType. Параметр bound=BaseModel означает, что в качестве конкретного значения этой переменной допускаются только такие типы, которые являются наследниками BaseModel.
Это условие имеет практический смысл. Функция get_config() будет получать на вход класс модели и вызывать у него метод model_validate(). Такой метод гарантированно существует у наследников BaseModel. Если бы ограничение bound=BaseModel отсутствовало, в функцию можно было бы передать любой класс, не обладающий нужным интерфейсом, и это сделало бы код менее строгим.
Пример:
from pydantic import BaseModel, SecretStrclass BotConfig(BaseModel): token: SecretStrclass DbConfig(BaseModel): host: str port: int
Обе модели наследуются от BaseModel, следовательно, обе могут быть подставлены вместо ConfigType.
6. Модель BotConfig. Описание структуры секции [bot]
Теперь рассмотрим класс:
class BotConfig(BaseModel): token: SecretStr
Это модель конфигурации бота. Она описывает структуру данных, которые должны быть извлечены из раздела [bot] файла settings.toml.
Если файл содержит:
[bot]token = "abc123"
то словарь секции будет иметь вид:
{"token": "abc123"}
После передачи этого словаря в модель BotConfig произойдёт валидация. Pydantic проверит наличие поля token и преобразует строку в значение типа SecretStr.
Пример:
from pydantic import BaseModel, SecretStrclass BotConfig(BaseModel): token: SecretStrdata = {"token": "abc123"}config = BotConfig.model_validate(data)print(config)print(config.token)print(config.token.get_secret_value())
Результат:
token=SecretStr('**********')**********abc123
Из этого примера видно, что модель выполняет сразу несколько функций. Она фиксирует структуру данных, обеспечивает проверку наличия обязательных полей и создаёт объект с удобным доступом к этим полям через атрибуты.
7. Функция parse_config_file(), механизм чтения всего файла
Теперь можно перейти к разбору первой функции.
@lru_cachedef parse_config_file() -> dict[str, Any]: config_path = Path(__file__).resolve().parent.parent.joinpath("settings.toml") if not config_path.exists(): error = "Could not find settings file" raise ValueError(error) with open(config_path, "rb") as file: config_data = load(file) return config_data
Эта функция отвечает за чтение конфигурационного файла целиком. Она не знает, какой именно раздел понадобится программе. Её задача состоит в том, чтобы найти файл, прочитать его и вернуть полученные данные в виде словаря.
7.1. Декоратор @lru_cache
Строка:
@lru_cache
означает, что результат функции будет кэшироваться. Так как функция не принимает аргументов, у неё существует только один вариант вызова. Следовательно, после первого чтения файла функция будет возвращать сохранённый словарь.
7.2. Объявление функции и возвращаемый тип
Строка:
def parse_config_file() -> dict[str, Any]:
обозначает, что функция не принимает параметров и возвращает словарь, у которого ключи имеют тип str, а значения могут иметь произвольную структуру.
7.3. Вычисление пути к settings.toml
Самая сложная строка функции:
config_path = Path(__file__).resolve().parent.parent.joinpath("settings.toml")
Её необходимо разобрать последовательно.
__file__ — это специальная переменная Python, содержащая путь к текущему файлу модуля. Если код находится, например, в файле:
/project/app/core/config.py
то __file__ указывает именно на этот путь.
Вызов Path(__file__) превращает строковый путь в объект Path.
Метод .resolve() преобразует путь в абсолютный и устраняет неоднозначности, связанные с относительными участками пути. Для конфигурационных задач это полезно, поскольку позволяет работать с нормализованным полным путём.
Свойство .parent означает переход к родительскому каталогу. Если исходный файл расположен по пути /project/app/core/config.py, то:
Path(__file__).resolve().parent
даст:
/project/app/core
Следующее .parent даст:
/project/app
После этого метод .joinpath("settings.toml") добавит к найденному каталогу имя файла конфигурации. В результате получится путь:
/project/app/settings.toml
Из этого следует важный вывод. Такое выражение корректно только в том случае, если файл settings.toml действительно лежит в каталоге /project/app. Если файл расположен в корне проекта /project/settings.toml, количество переходов .parent должно быть иным.
Рассмотрим пример:
from pathlib import Pathcurrent_file = Path("/project/app/core/config.py")print(current_file.parent)print(current_file.parent.parent)print(current_file.parent.parent.joinpath("settings.toml"))
Результат:
/project/app/core/project/app/project/app/settings.toml
Следовательно, путь к конфигурации всегда должен строиться в строгом соответствии с фактической структурой проекта.
7.4. Проверка существования файла
Далее выполняется проверка:
if not config_path.exists(): error = "Could not find settings file" raise ValueError(error)
Метод .exists() возвращает логическое значение. Если путь существует, возвращается True. Если не существует, возвращается False.
Оператор not инвертирует это значение. Следовательно, условие означает: если файл не найден, необходимо прервать выполнение и сообщить об ошибке.
Переменная error хранит текст сообщения, после чего инструкция raise ValueError(error) выбрасывает исключение ValueError.
Пример подобного поведения:
number = Noneif number is None: raise ValueError("Number is missing")
Как только выполняется raise, обычный ход работы функции прекращается.
Если конфигурационный файл отсутствует, дальнейшая работа программы невозможна, поэтому ошибка должна выявляться сразу.
7.5. Открытие файла в бинарном режиме
Следующий фрагмент:
with open(config_path, "rb") as file:
Функция open() открывает файл. Первый аргумент — путь к файлу. Второй аргумент "rb" означает режим чтения в бинарной форме.
Ключевое слово with вводит контекстный менеджер. Его назначение состоит в том, чтобы гарантировать корректное закрытие файла после завершения блока. Даже если внутри блока произойдёт исключение, файл будет закрыт автоматически.
Пример:
with open("example.txt", "rb") as file: data = file.read()
После выхода из блока with файл больше не остаётся открытым.
7.6. Разбор TOML в словарь Python
Внутри блока выполняется строка:
config_data = load(file)
Функция load() читает содержимое файла, интерпретирует его как TOML и возвращает словарь Python.
Если файл содержит:
[bot]token = "abc123"[db]host = "localhost"port = 5432
то config_data примет вид:
{ "bot": { "token": "abc123" }, "db": { "host": "localhost", "port": 5432 }}
Следовательно, именно на этом этапе текстовая конфигурация превращается в стандартные структуры Python.
7.7. Возврат результата
Последняя строка:
return config_data
возвращает словарь вызывающему коду.
Таким образом, функция parse_config_file() изолированно выполняет чтение и синтаксический разбор конфигурационного файла, но не занимается интерпретацией конкретных разделов.
8. Функция get_config()
Теперь рассмотрим вторую функцию.
def get_config(model: Type[ConfigType], root_key: str) -> ConfigType: config_dict = parse_config_file() if root_key not in config_dict: error = f"Key {root_key} not found" raise ValueError(error) return model.model_validate(config_dict[root_key])
Эта функция работает на следующем уровне абстракции. Она не читает файл напрямую, а получает общий словарь конфигурации, извлекает нужную секцию и создаёт объект переданной модели.
8.1. Параметр model
Параметр:
model: Type[ConfigType]
означает, что в функцию должен быть передан класс модели, а не экземпляр этой модели.
Например, допустим такой вызов:
get_config(BotConfig, "bot")
Здесь передаётся именно класс BotConfig, а не объект BotConfig(...).
Это важно, потому что функция сама должна вызвать у класса метод model_validate() и самостоятельно создать объект.
8.2. Параметр root_key
Параметр:
root_key: str
обозначает имя корневого раздела конфигурации, который требуется извлечь из словаря.
Если файл содержит:
[bot]token = "abc123"
то для этого раздела имя корневого ключа будет "bot".
8.3. Возвращаемый тип функции
Аннотация:
-> ConfigType
означает, что функция вернёт объект того же типа, класс которого был передан в параметре model.
Если передан BotConfig, функция вернёт объект BotConfig. Если передан DbConfig, функция вернёт объект DbConfig.
В этом и состоит практический смысл использования TypeVar.
8.4. Получение полного словаря конфигурации
Строка:
config_dict = parse_config_file()
вызывает ранее разобранную функцию чтения конфигурации.
При первом вызове будет произведено чтение файла. При последующих вызовах будет использован кэшированный результат.
8.5. Проверка наличия нужного раздела
Далее выполняется проверка:
if root_key not in config_dict: error = f"Key {root_key} not found" raise ValueError(error)
Оператор in при работе со словарём проверяет наличие ключа.
Пример:
data = {"bot": {"token": "abc"}}print("bot" in data)print("db" in data)
Результат:
TrueFalse
Если нужного раздела нет, выбрасывается исключение ValueError.
8.6. Валидация данных и создание объекта модели
Центральная строка функции:
return model.model_validate(config_dict[root_key])
Сначала выполняется извлечение из общего словаря секции с именем root_key. Если root_key == "bot", результатом будет вложенный словарь, например:
{"token": "abc123"}
Затем у переданного класса модели вызывается метод model_validate(). Этот метод принимает обычный словарь и выполняет две операции одновременно. Он проверяет, соответствует ли словарь структуре модели, и создаёт экземпляр модели.
Пример:
from pydantic import BaseModelclass DbConfig(BaseModel): host: str port: intdata = {"host": "localhost", "port": 5432}config = DbConfig.model_validate(data)print(config)print(config.host)print(config.port)
Результат:
host='localhost' port=5432localhost5432
Если данные не соответствуют модели, будет выброшено исключение валидации.
Пример некорректных данных:
from pydantic import BaseModelclass DbConfig(BaseModel): host: str port: intdata = {"host": "localhost", "port": "not_a_number"}DbConfig.model_validate(data)
В этом случае поле port не соответствует ожидаемому типу int, и модель сообщит об ошибке.
Следовательно, функция get_config() не просто извлекает словарь из словаря, а выполняет типизированное преобразование внешних данных в объект прикладного уровня.
9. Полный путь данных от файла до объекта
Теперь необходимо соединить все рассмотренные части в единый процесс.
Пусть файл settings.toml имеет вид:
[bot]token = "super_token"
Пусть программа вызывает:
config = get_config(BotConfig, "bot")
Последовательность работы будет следующей.
Сначала функция get_config() получает два аргумента: класс BotConfig и строку "bot".
Затем вызывается parse_config_file().
Внутри parse_config_file() строится путь к settings.toml. После этого проверяется существование файла. Если файл найден, он открывается в бинарном режиме. Функция load() читает его и возвращает словарь:
{"bot": {"token": "super_token"}}
Этот словарь возвращается в get_config() и сохраняется в переменной config_dict.
Затем выполняется проверка наличия ключа "bot" в словаре. После этого выражение:
config_dict[root_key]
даёт:
{"token": "super_token"}
Далее выполняется:
BotConfig.model_validate({"token": "super_token"})
В результате создаётся объект модели BotConfig, в котором поле token хранится как SecretStr.
Пример использования:
config = get_config(BotConfig, "bot")print(config)print(config.token)print(config.token.get_secret_value())
Результат:
token=SecretStr('**********')**********super_token
Таким образом, исходная текстовая конфигурация проходит путь от файла TOML через словарь Python к типизированному объекту прикладного уровня.
10. Расширение конфигурации несколькими секциями
Рассматриваемый подход не ограничивается одной секцией [bot]. Его основное достоинство в том и состоит, что одна и та же функция get_config() может работать с разными моделями.
Пусть файл settings.toml выглядит так:
[bot]token = "super_token"[db]host = "localhost"port = 5432
Теперь можно описать ещё одну модель:
from pydantic import BaseModelclass DbConfig(BaseModel): host: str port: int
После этого можно получить две независимые конфигурации:
bot_config = get_config(BotConfig, "bot")db_config = get_config(DbConfig, "db")print(bot_config.token.get_secret_value())print(db_config.host)print(db_config.port)
Результат:
super_tokenlocalhost5432
Здесь особенно важно заметить, что общий словарь файла читается только один раз. При первом вызове get_config() функция parse_config_file() действительно открывает файл. При втором вызове срабатывает lru_cache, и уже сохранённый словарь используется повторно.
11. Подробный пример полного модуля
Ниже приведён развёрнутый вариант с двумя моделями.
from functools import lru_cachefrom pathlib import Pathfrom tomllib import loadfrom typing import Any, Type, TypeVarfrom pydantic import BaseModel, SecretStr# Универсальная переменная типа.# Она ограничена BaseModel, поэтому вместо неё можно подставлять# только классы моделей Pydantic.ConfigType = TypeVar("ConfigType", bound=BaseModel)# Модель секции [bot].class BotConfig(BaseModel): token: SecretStr# Модель секции [db].class DbConfig(BaseModel): host: str port: int@lru_cachedef parse_config_file() -> dict[str, Any]: # Путь к settings.toml строится относительно текущего файла. config_path = Path(__file__).resolve().parent.joinpath("settings.toml") # Если файл не найден, дальнейшая работа невозможна. if not config_path.exists(): raise ValueError("Could not find settings file") # Файл открывается в бинарном режиме, как того требует tomllib.load(). with open(config_path, "rb") as file: config_data = load(file) # Возвращается полный словарь конфигурации. return config_datadef get_config(model: Type[ConfigType], root_key: str) -> ConfigType: # Получение всего словаря конфигурации. config_dict = parse_config_file() # Проверка наличия нужного корневого раздела. if root_key not in config_dict: raise ValueError(f"Key {root_key} not found") # Проверка структуры данных и создание объекта модели. return model.model_validate(config_dict[root_key])bot_config = get_config(BotConfig, "bot")db_config = get_config(DbConfig, "db")print(bot_config)print(bot_config.token.get_secret_value())print(db_config)print(db_config.host)print(db_config.port)
Если файл settings.toml содержит:
[bot]token = "telegram_secret_token"[db]host = "127.0.0.1"port = 5432
то результат выполнения будет таким:
token=SecretStr('**********')telegram_secret_tokenhost='127.0.0.1' port=5432127.0.0.15432
Конфигурация разделена на секции. Каждая секция описывается собственной моделью. Общий механизм чтения файла является единым. Проверка структуры производится централизованно. Результат предоставляется в объектной форме.
12. Практический смысл Pydantic в конфигурационных задачах
Теоретически можно было бы читать конфигурацию без Pydantic и обращаться к словарю напрямую:
config = parse_config_file()token = config["bot"]["token"]
Однако такой подход уступает модельному способу по нескольким основаниям.
Во-первых, структура данных нигде явно не фиксируется. Программист должен помнить, какие поля обязаны существовать и какого типа они должны быть.
Во-вторых, ошибка в конфигурации обнаруживается только в момент обращения к конкретному ключу, а не в одном централизованном месте.
В-третьих, объектная форма доступа к данным обычно удобнее и нагляднее, чем работа с вложенными словарями.
Сравнение:
config["bot"]["token"]
и
bot_config.token
Во втором случае структура более ясна. Кроме того, сама модель уже является частью документации кода.
В-четвёртых, SecretStr позволяет снизить риск случайного вывода секрета в консоль или журнал.
Следовательно, Pydantic в данном модуле выполняет конструктивную роль: он превращает внешние данные в описанные и проверенные объекты.
14. Итоговый вариант модуля
from functools import lru_cachefrom pathlib import Pathfrom tomllib import loadfrom typing import Any, Type, TypeVarfrom pydantic import BaseModel, SecretStrConfigType = TypeVar("ConfigType", bound=BaseModel)class BotConfig(BaseModel): token: SecretStr@lru_cachedef parse_config_file() -> dict[str, Any]: config_path = Path(__file__).resolve().parent.parent.joinpath("settings.toml") if not config_path.exists(): error = "Could not find settings file" raise ValueError(error) with open(config_path, "rb") as file: config_data = load(file) return config_datadef get_config(model: Type[ConfigType], root_key: str) -> ConfigType: config_dict = parse_config_file() if root_key not in config_dict: error = f"Key {root_key} not found" raise ValueError(error) return model.model_validate(config_dict[root_key])
ссылка на оригинал статьи https://habr.com/ru/articles/1022336/