Как сделать config в python
Перейти к содержимому

Как сделать config в python

  • автор:

Конфигурационные файлы в Python

Конфиги. Все хранят их по разному. Кто-то в .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() # . 

Итоги

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

P.S

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

Python: конфигурация проекта без боли

Расскажу о проделанном пути, чтобы найти идеальный, для моих целей, инструмент конфигурирования проекта и о создании легковесной библиотеки bestconfig, впитавшей в себя преимущества изложенных подходов.

В статье речь пойдет только о локальных способах хранения настроек, здесь не разбираются случаи загрузки из сети.

После создания проекта рано или поздно возникает вопрос: куда записывать номер версии, где хранить токены, пароли, настройки, каким форматом файлов конфигурации воспользоваться: .json , .yaml , .env, .cfg , .ini или просто создать config.py и записывать туда переменные?

Для каждого из перечисленных вариантов есть библиотека на python, приведу примеры самых популярных форматов.

ENV

Переменные окружения могут передаваться программе напрямую командным интерпретатором: они записаны в .bashrc , выполнена команда export , или просто указаны в интерфейсе хостинга. Для того, чтобы их явно указать, удобно использовать .env файлы в директории проекта. Если прописать .env в . gitignore можно сложить туда все ключи и не боятся их опубликовать, в то время как остальные настройки будут в другом месте для использования в продакшн окружении

VERSION=2.5.11 PG_USER=postgres PG_DATABASE=my_project

Библиотека dotenv отлично справляется с задачей подгрузки таких файлов. Все переменные сразу попадают в os.environ

from dotenv import load_dotenv import os load_dotenv() print(os.getenv('VERSION'))

YAML

В последнее время стал популярен формат .yml , как один из самых приятных для чтения человеком

version: 2.5.11 logger: level: INFO format: '%(asctime)-15s %(clientip)s %(user)-8s %(message)s'

Открываем файл из кода

from yaml import load with open('config.yml', 'r') as f: data = load(f) print(data['version'])

JSON

Формат чаще используется для коммуникации между сервисам, например между фронтом и беком, как компактный, но в то же время понятный человеку. Но задать в нем некоторые настройки тоже можно

import json with open('config.json', 'r') as f: data = json.load(f) print(data['version'])

PY

В файле с расширением .py объявляем все необходимые переменные. У данного подхода есть преимощество — можно некоторые значения вычислять на ходу: инкриментировать номер сборки, менять хост в зависимости от режима: DEBUG или RELEASE и тд.

VERSION = '2.5.11' PG_DATABASE = 'postgres' PG_PORT = 5432

Осталось только импортировать этот файл там, где он нужен

from config import VERSION print(VERSION)

INI

[DEFAULT] version = '2.5.11' [postgres] user = "postgres" database = "my-project"
import configparser config = configparser.ConfigParser() config.read('settings.ini') print(config["version"])

Все просто, разве нет?

Самое интересное начинается, когда в проекте появляется несколько источников конфигов. Например статичные настройки лежат в config.yml , токены и пароли в переменных окружения и .env , некоторые записаны в файле env_file , используемом докером, часть значений нужно переопределить из кода, так как они заранее не известны, часть извлечь из аргументов программы.

Это ведет за собой менее красивый код, который обязан учитывать особенности хранения, приходится задумаваться, откуда мы берем очередное значения настроек, появляется несогласованность, ведь в одних местам мы обращаеся к os.environ , в других к глобальному словарю, в третьих, к сторонней библиотеке.

BestConfig

Блуждая по просторам всемирной паутины, идеального решения я не нашел и создал очень простую и удобную библиотеку, умеющую бороться в вышеперечисленными проблемами.

Поставил целью уменьшить количество кода чтения конфигов почти до нуля, но в тоже время оставить гибкость и возможность подкрутки под более специфичные задачи. Итак, давайе посмотрим, как импортировать например config.yml

from bestconfig import Config config = Config() print(config['version'])

И да, это все, что нужно сделать. Глобальный объект config уже будет содержать все необходимое. А теперь по порядку.

Что происходит при создании Config() :

  1. Запускается поиск файлов, похожих на конфиги: любые комбинации имени conf, config, configuration, setting, settings с расширениями yml, yaml, json, ini, cfg, env в текущей директории и в папках выше, вплодь до корня проекта. Также в хранилище добавляются переменные окружения
  2. Исходя из формата файла производится импорт содержимого
  3. Создается класс ConfigProvider , предоставляющий универсальный доступ ко всему содержимому
print(isinstance(Config(), ConfigProvider)) # True

Посмотрим на примерах, в чем же «универсальность» объекта config

Обращение по ключу

print(config.get('version')) # При отсутствии возвращает None print(config['verions']) # При отсутствии бросает KeyError print(config.version) # Тоже может бросить KeyError # Вложенные структуры print(config.postgres.port) print(config['postgres.port']) print(config.get('postgres.port')) # Очевидно, глубина вложенности не ограничена

Иногда мы хотим быть уверены, что значение принадлежит определенному типу

type(config.int('limit')) # int type(config.dict('logger')) # dict type(config.list('admins')) # list # И тд: float, str

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

Иногда хочется где-нибудь в самом начале программы или скрипта убедиться, что необходимые переменные заданы, для этого предусмотренна специальная функция

config.assert_contains('key') # Что эквивалентно assert config.get('key')

Класс ConfigProvider наследуется от dict , поэтому его можно спокойно передавать в сторонние библиотеки.

import logging.config from config import config logging.config.dictConfig(config['logging']) print(config) # Выведется как словарь

Модификация

from argparse import ArgumentParser from config import config if __name__ == '__main__': # Парсим аргументы командной строки parser = ArgumentParser() parser.add_argument('log_level') args = parser.parse_args() # Обновляем общий конфиг config.insert()

Помимо словаря метод insert принимает названия файлов (с абсолютным или относильным путем) любого из поддерживаемых расширений

config.insert('logging.json') # Кроме обычных методов dict-а есть set config.set('key', 'value')

Выше я приводил пример, в котором конфиг переменные объявляются прямо в .py файле. Если в проекте уже используется такой вариант, необязательно всё менять, на этот случай есть метод update_from_locals

from config import config VERSION = '2.5.11' LOGGING_LEVEL = 'INFO' config.update_from_locals()

В питоне есть встроенная функци locals() , она возвращает словарь локальных переменных, имеено ее и использует данный метод. Там, конечно, рассматриваются добавляемые объекты и лишние отсеиваются (подробнее в документации).

Кастомизация

По умолчанию Config() делает довольно много всего, с целью быть универсальным, но содежит ряд аргументов, регулирующих это поведение.

# Исключить из списка по умолчанию некоторые файлы Config(exclude=['server/setting.json']) # Указать свои пути Config('my-config.json', '../other-config.yml', 'app/settings.cfg') # Импортировать только config.yml и кидать исключение при его отсутствии # Игнорировать весь список по умолчанию Config('config.yml', exclude_default=True, raise_on_absent=True)

Установка

pip install bestconfig

Best practices

Обобщу процесс взаимодействия с конфигами с использованием библиотеки bestconfig.

  1. Статичные настройки, не содержащие ключей лежат в файле config.yml в корне проекта
  2. Переменные окружения и ключи задаются в .env и игнорируются системой контроля версий
  3. Файл config.py содержит создает класс Config и выполняет другие настройки (например логгирование, версия проекта, обработка параметров запуска и тд)
  4. В других местах проекта делаем from config import config и используемый нужные данные удобным образом

Резюме

Библиотека создана чтобы максимально упростить распространенную задачу по обращению к файлам настроки, ее основные фичи и особенности:

  • Поддержка множества форматов файлов
  • Интерфейс доступа к данным «на любой вкус» (по ключу, через точку, через get и тд)
  • Маленький вес (примерно 15 кб)
  • Большое тестовое покрытие
  • Чистый, типизированный, расширяемый python код на модульной архитектуре
  • Открытость к изменениям и дополнениям (pull request-ы приветствуются)

Есть и недостатки:

  • У проекта появляются лишние зависимости. Например вы не используете yaml, однако библиотека подтягивает pyyaml
  • Захват лишнего. Вы можете написать Config() и даже не обратить внимание, сколько всего попало в config, одни переменные окружения чего стоят — их может быть очень много. И вывод print(config) становится абсолютно не читаемым

Теперь, при создании нового sandbox проекта или усложении текущего еще одним видом конфигурационных файлов, вы можете выбрать из своего арсенала еще один инструмент!

  • Python
  • Совершенный код

От новичка в эксперты: пишем файл конфигурации для вашего Python приложения

Относитесь к файлу конфигурации, использующемуся в приложении, как к разрабатываемому вами коду.

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

Разработка качественного программного обеспечения, включает заботу о разработке своей экосистемы, например, для организации процессов тестирования, развертывания, сетевого обмена данными и т.д. Одним из наиболее важных аспектов, который необходимо при этом учитывать, является реализация гибкого механизма управления конфигурацией (настройками) программного обеспечения.

Правильная реализация управления конфигурацией по сути должна позволять запускать программное обеспечение в любой среде без внесения изменений в его исходный код. Этот подход обеспечивает эффективное управление проблемными настройками вашего приложения со стороны администраторов Ops, обеспечивает представление информации о том, что может произойти во время его функционирования, а также позволяет изменять его поведение во время выполнения.

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

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

Когда необходим файл конфигурации приложения?

Перед разработкой конфигурационного файла сначала необходимо спросить себя, нужен ли вообще какой-либо внешний файл с данными? Разве мы не можем просто поместить их в виде константных значений прямо в исходном коде? Собственно, достаточно известная концепция The Twelve-Factor App давно отвечает на этот вопрос:

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

Обратите внимание, что это определение config не включает внутреннюю конфигурацию приложения, такую как, например, как config/routes.rb в Rails, или способ подключения модулей в Spring. Перечисленные выше примеры способов конфигурации не меняются в зависимости от среды развертывания, и поэтому это лучше всего реализовать их в коде.

Подходы рекомендованные этой концепцией предписывают, чтобы любые параметры, зависящие от среды, такие как учетные данные базы данных, находились во внешнем файле. В противном случае их реализуют просто обычными константами в коде. Другой вариант использования, который я часто вижу, – это хранение динамических переменных (данных) во внешнем файле (базе данных), например, черный blacklist или белый whitelist список пользователей. Ими могут быть числа в заданном диапазоне (например, длительность тайм-аута) или любые текстовые файлы с произвольным содержимым. Отметим, что эти динамические переменные (данные) остаются неизменными вне зависимости от особенностей исполняемой среды.

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

Какой формат файла конфигурации использовать?

С практической точки зрения, на формат файла конфигурации нет никаких технических ограничений, если код приложения может его прочитать и анализировать. Но есть и более рациональные практики для выбора формата файла с настройками. Так наиболее распространенными, стандартизованными форматами являются YAML, JSON, TOML и INI. Самый подходящий формата для файл конфигурации должен соответствовать как минимум трем критериям:

  1. Быть легко читаемым и редактируемым: файл должен иметь текстовый формат и такую структуру, чтобы его содержимое было легко понятно даже не разработчику.
  2. Разрешать использование комментариев: файл конфигурации – это то, что могут читать не только разработчики. Поэтому в процессе эксплуатации приложения чрезвычайно важно когда пользователи могут успешно пытаться понять его работу и изменить его поведение. Написание комментариев – это эффективный способ быстро пояснить ключевые особенности настройки приложения и делает конфигурационный файл более выразительным.
  3. Простота развертывания: файл конфигурации должен понятен для обработки всеми операционными системами и средами. Он также должен легко доставляться на сервер с помощью конвейера pipeline CDaaS.

Возможно вам пока не ясно какой из форматов файла лучше использовать. Но если вы подумаете об этом в контексте программирования на языке Python, то наиболее очевидным ответом будет YAML или INI. Форматы YAML и INI хорошо понятны большинству программ и пакетов Python.

INI файл, вероятно, является наиболее простым решением для сохранения настроек приложения, имеющих только один уровень иерархии (вложенности) параметров. Однако формат INI не поддерживает других типов данных, кроме строк: в нем все данные имеют строковое представление.

[APP] ENVIRONMENT = test DEBUG = True # Принимает значение только True или False [DATABASE] USERNAME = xiaoxu PASSWORD = xiaoxu HOST = 127.0.0.1 PORT = 5432 DB = xiaoxu_database

Та же конфигурация настроек в YAML выглядит следующим образом.

APP: ENVIRONMENT: test DEBUG: True # Принимает значение только True или False DATABASE: USERNAME: xiaoxu PASSWORD: xiaoxu HOST: 127.0.0.1 PORT: 5432 DB: xiaoxu_database

Как видите, YAML изначально поддерживает использование вложенные структуры (также как и JSON) с помощью отступов. Кроме того, YAML, в отличие от формата INI файлов, поддерживает некоторые другие типы данных такие как целые и с плавающей запятой числа, логические значения, списки, словари и т.д.

Формат файлов JSON по сути очень похож на YAML и тоже чрезвычайно популярен, однако в JSON файлы нельзя добавлять комментарии. JSON, как текстовый формат содержащий структурированные данные, часто используется для хранения внутренней конфигурации внутри программы, но совершенно не предназначен для того, чтобы делиться конфигурацией приложения с другими людьми (в особенности с далекими от вопросов разработки ПО).

< "APP": < "ENVIRONMENT": "test", "DEBUG": true >, "DATABASE": < "USERNAME": "xiaoxu", "PASSWORD": "xiaoxu", "HOST": "127.0.0.1", "PORT": 5432, "DB": "xiaoxu_database" >>

Формат TOML, с другой стороны, похож на INI, но поддерживает гораздо больше типов данных, а также специальный синтаксис для хранения вложенных структур. Его часто используют менеджеры пакетов Python такие, например, pip или poetry. Но если в файле конфигурации присутствует слишком много вложенных структур, то YAML в этом отношении, с моей точки зрения, наилучший выбор. Следующий ниже фрагмент файла выглядит как INI, но в отличие от него каждое строковое значение имеет кавычки.

[APP] ENVIRONMENT = "test" DEBUG = true # Only accept True or False [DATABASE] USERNAME = "xiaoxu" PASSWORD = "xiaoxu" HOST = "127.0.0.1" PORT = 5432 DB = "xiaoxu_database"

Пока что мы выяснили ЧТО из себя представляют форматы файлов YAML, JSON, TOML и INI, далее мы рассмотрим КАК они могут быть использованы.

YAML/JSON — простое чтение внешнего файла

Как обычно, мы начнем с самого простого, то есть создадим внешний файл с настройками, а затем прочитаем его. Python имеет в своем составе встроенные пакеты для чтения и анализа файлов YAML и JSON. И как видно из приведенного ниже кода, они фактически возвращают один и тот же объект типа dict, поэтому доступ к его атрибутам будет одинаковым для обоих файлов.

Чтение

Из-за проблем с безопасностью рекомендуется использовать метод yaml.safe_load() вместо yaml.load() , чтобы избежать внедрения вредоносного кода при чтении файла конфигурации.

import json import yaml def read_json(file_path): with open(file_path, "r") as f: return json.load(f) def read_yaml(file_path): with open(file_path, "r") as f: return yaml.safe_load(f) assert read_json("data/sample.json") == read_yaml("data/sample.yaml")

Валидация

При использовании обоих пакетов при попытке чтения несуществующего файла будет генерироваться исключение типа FileNotFoundError . Использование пакета для чтения файлов YAML позволяет получать разные исключения для следующих случаев: пользователь указал файл не являющимся YAML файлом, а также прочитанный файл YAML является не корректным, то есть содержит синтаксические ошибки. В свою очередь пакет для чтения JSON файлов генерирует единственное исключение типа JSONDecoderError для обоих рассмотренных случаев.

import pytest def test_validation_json(): with pytest.raises(FileNotFoundError): read_json(file_path="source/data/non_existing_file.json") with pytest.raises(json.decoder.JSONDecodeError): # only show the first error read_json(file_path="source/data/sample_invalid.json") read_json(file_path="source/data/sample_invalid.yaml") def test_validation_yaml(): with pytest.raises(FileNotFoundError): read_yaml(file_path="source/data/non_existing_file.yaml") with pytest.raises(yaml.scanner.ScannerError): # only show the first error read_yaml(file_path="source/data/sample_invalid.yaml") with pytest.raises(yaml.parser.ParserError): # only show the first error read_yaml(file_path="source/data/sample_invalid.json")

Пакет Cofigureparser из состава стандартной библиотеки Python

В этом разделе рассмотрим пакеты, предназначенные непосредственно для управления конфигурацией приложения. И начнем со встроенного в стандартную библиотеку Python пакета: Configureparser.

Configureparser в большинстве случаев используется для чтения и записи INI файлов, и поддерживает чтение входных данных из файла сразу в виде словаря или итерируемого iterable файлоподобного объекта. Как известно, каждый файл INI состоит из нескольких секций, содержащих настройки в виде пар ключ-значение. Ниже приведен простой пример кода для доступа к полям настроек.

Чтение

import configparser def read_ini(file_path, config_json): config = configparser.ConfigParser() config.read(file_path) for section in config.sections(): for key in config[section]: print((key, config[section][key])) read_ini("source/data/sample.ini", config_json) # ('environment', 'test') # ('debug', 'True') # ('username', 'xiaoxu') # ('password', 'xiaoxu') # ('host', '127.0.0.1') # ('port', '5432') # ('db', 'xiaoxu_database')

Как видно из примера Configureparser не может «угадать» типы данных, содержащихся в файле конфигурации, так как значение каждой настройки сохраняется в виде строки. Тем не менее, он предоставляет несколько полезных методов для преобразования строк (значений настроек) в нужный тип данных. Наиболее интересным из них является метод преобразующий значения в логический тип, то есть он может распознавать некоторые логические значения, например, yes / no , on / off , true / false и 1 / 0 .

Как уже нами упоминалось ранее, Configureparser может читать данные настроек в следующих видах на выходе: словаря с помощью метода read_dict() , простой строки с использованием read_string() и итерируемого файлоподобного объекта, возвращаемого методом read_file() .

import configparser def read_ini_extra(file_path, dict_obj=None): config = configparser.ConfigParser() if dict_obj: config.read_dict(dict_obj) else: config.read(file_path) debug = config["APP"].getboolean("DEBUG") print(type(debug)) # name = config.get('APP', 'NAME', fallback='NAME is not defined') print(name) return debug # читаем ini файл read_ini_extra(file_path="source/data/sample.ini") # читаем данные в словарь config_json = read_json(file_path="source/data/sample.json") read_ini_extra(dict_obj=config_json)

Валидация

Валидация данных с Configureparser не так проста, как для пакетов, работающих с форматами YAML и JSON. Во-первых, он не возбуждает исключения FileNotFoundError если файла настроек не существует, а вместо этого вызывает исключение типа KeyError, как при попытке доступа к отсутствующему ключу.

Кроме того, этот пакет «игнорирует» некоторые ошибки форматирования, например, неправильное использование отступа. Так в приведенном ниже примере, в случае если в файле присутствует дополнительная табуляция или пробел перед настройкой DEBUG , то вы получите неправильные значения для обеих настроек ENVIRONMENT и DEBUG .

ENVIRONMENT=test DEBUG=true USERNAME=xiaoxu PASSWORD=xiaoxu HOST=127.0.0.1 PORT=5432

Тем не менее, Configureparser может возбуждать исключение ParserError при наличии нескольких ошибок (см. пример кода с тестами ниже). И в большинстве случаев этого достаточно для определения проблемных мест в самом файле настроек.

import pytest def test_validation_configureparser(): # не возбуждается FileNotFoundError, возбуждается KeyError # как и для случая отсутствия ключа (параметра настройки) with pytest.raises(KeyError): read_ini_extra(file_path="source/data/non_existing_file.ini") # [APP] # ENVIRONMENT = test # DEBUG = True # не возбуждается исключение из-за неверной расстановки отступов debug = read_ini_extra( file_path="source/data/sample_wrong_indentation.ini" ) print(debug) # None # Однако, config["APP"]["ENVIRONMENT"] вернет 'test\nDEBUG = True' # [APP] # ENVIRONMENT = test # DEBUG True # [DATABASE] # USERNAME = xiaoxu # PASSWORD xiaoxu with pytest.raises(configparser.ParsingError): debug = read_ini_extra( file_path="source/data/sample_wrong_key_value.ini" ) # выведет все ошибки # configparser.ParsingError: Source contains parsing errors: 'source/data/sample_wrong_key_value.ini' # [line 3]: 'DEBUG True\n' # [line 8]: 'PASSWORD xiaoxu\n'

Python-dotenv — считываем конфигурацию приложения из переменных среды

Теперь перейдем к сторонним библиотекам, использующимся для управления конфигурацией приложений Python. До сих пор я намеренно пропустил еще один тип файлов конфигурации, а именно .env . Так значения настроек, находящихся в файле .env при запуске терминала (скрипта приложения) будут загружены как переменные среды, и поэтому с помощью библиотеки python-dotenv , а точнее ее метода os.getenv() можно получить доступ к ним из кода приложения.

Файл .env обычно выглядит следующим образом. По умолчанию его местонахождение – корневая папка вашего проекта.

ENVIRONMENT=test DEBUG=true USERNAME=xiaoxu PASSWORD=xiaoxu HOST=127.0.0.1 PORT=5432

Чтение

Этот тип файла конфигурации очень легко использовать. Так если вы решите переопределить существующую (или создать новую) переменную среды, то можете использовать вызов метод load_dotenv() , например, зададим значение параметра override .

import os from dotenv import load_dotenv load_dotenv() print(os.getenv('DEBUG')) # true load_dotenv(override=True) # переопределяем переменную среды

Валидация

Тем не менее пакет python-dotenv не проверяет корректность .env файла. Допустим у вас есть некоторый .env файл (его содержимое представлено ниже), и вы хотите получить доступ к значению переменной (параметра настройки) DEBUG , то будет возвращено значение None без возбуждения исключения соответствующего типа.

# .env ENVIRONMENT=test DEBUG # load.py load_dotenv() print('DEBUG' in os.environ.keys()) # False

Dynaconf — мощный конфигуратор настроек для приложений Python

Dynaconf — это очень мощная система для конфигурации настроек в Python, которая поддерживает следующие форматы файлов: yaml, json, ini, toml и python. Она также позволяет автоматически загружать .env файл и поддерживает настраиваемые правила для валидации данных настроек. Проще говоря, он охватывает практически весь функционал трех предыдущих рассмотренных вариантов и даже выходит за рамки этого. Например, вы можете сохранить зашифрованный пароль и используя специальный загрузчик для его расшифровки. Он прекрасно интегрирован с Flask, Django и Pytest. Я не буду упоминать все его возможности в этой статье, и поэтому для более подробной информации обратитесь к его документации.

Чтение

Dynaconf использует .env файл для поиска других конфигурационных файлов и последующего заполнения полей settings объекта с настройками. Так если в двух файлах настроек есть одна и та же переменная, то ее значение будет перезаписано значением из последнего файла настроек.

# .env # ROOT_PATH_FOR_DYNACONF="config/" # SETTINGS_FILE_FOR_DYNACONF="['settings.ini', 'settings2.ini']" from dynaconf import settings print(settings["DB"]) # xiaoxu_database

Валидация

Одна из наиболее интересных, в частности для меня, возможностей dynaconf – это его настраиваемый валидатор. Как упоминалось ранее, Configureparser недостаточно строго проверяет корректность INI файлов настроек, но это можно легко реализовать в dynaconf. В следующем примере мы проверяем, существуют ли определенные ключи в файле с настройками и имеется ли в нем конкретный ключ с корректным значением. Если вы читаете настройки из файла YAML или TOML, которые как мы говорили ранее, поддерживают несколько типов данных, то вы даже можете проверить, находится ли значение настройки, например, число в заданном диапазоне.

# settings.ini # [default] # ENVIRONMENT = test # DEBUG = True # USERNAME = xiaoxu # PASSWORD = xiaoxu # HOST = 127.0.0.1 # PORT = 5432 # DB = xiaoxu_database # [production] # DEBUG = False from dynaconf import settings, Validator settings.validators.register( Validator('ENVIRONMENT', 'DEBUG', 'USERNAME', must_exist=True), Validator('PASSWORD', must_exist=False), Validator('DEBUG', eq='False', env='production') ) # запускаем валидатор settings.validators.validate() # dynaconf.validator.ValidationError: PASSWORD cannot exists in env test

Интеграция с Pytest

Еще одна интересная особенность dynaconf – это возможность его интеграции с pytest. Так настройки для модульного тестирования unit testing обычно существенно отличаются в различных средах. Для этого вы можете использовать параметр FORCE_ENV_FOR_DYNACONF , чтобы ваше приложение могло прочитать значения настроек из внешнего файла, или использовать фикстуру monkeypatch для замены определенных пар ключ и значение в файле настроек.

import pytest from dynaconf import settings @pytest.fixture(scope="session", autouse=True) def set_test_settings(): settings.configure(FORCE_ENV_FOR_DYNACONF="testing") def test_dynaconf(monkeypatch): monkeypatch.setattr(settings, 'HOST', 'localhost')

Обновляем конфигурацию приложения во время его выполнения

Dynaconf в своем составе содержит метод reload() , который очищает значения настроек и перезапускает все загрузчики вашего приложения. Это полезно, если вы хотите, чтобы приложение перезагружало файл настроек во время выполнения и соответственно в последствие изменяло свое поведение. Например, приложение должно автоматически перезагрузить настройки, если файл конфигурации был открыт и изменен (откорректирован).

Hydra — упрощаем разработку, динамически создавая иерархическую структуру конфигурации приложения

Рассмотрим последним, в этой статье, способ создания и поддержки конфигурации для вашего приложения, который по сути является гораздо большим, чем просто загрузчик и парсер файлов с настройками.

Hydra – это платформа, разработанная Facebook для гибкой и элегантной настройки самых сложных приложений. Которая помимо чтения, записи и валидации корректности файлов конфигурации, реализовывает свою достаточно рациональную стратегию упрощения управления несколькими конфигурационными файлами, переопределения (перезаписи) их с использованием интерфейса командной строки, создания snapshot снимка состояния приложения перед каждым его запуском (между перезапусками) и т.д.

Чтение

Рассмотрим основы использования hydra. Так в следующем примере команда +APP.NAME , добавленная в командную строку при запуске скрипта, позволяет добавить новое поле (настройку) в конфигурацию приложения, а также осуществить перезапись значения существующего поля (значения настройки) APP.NAME=hydra1.1 .

import hydra from omegaconf import DictConfig, OmegaConf @hydra.main(config_name="config") def my_app(cfg: DictConfig) -> None: print(OmegaConf.to_yaml(cfg)) if __name__ == "__main__": my_app() # запускаем в командной строке скрипт с командами # python3 source/hydra_basic.py +APP.NAME=hydra # # результат его выполнения # APP: # ENVIRONMENT: test # DEBUG: true # NAME: hydra1.1

Валидация

Hydra прекрасно интегрируется с декоратором @dataclass для выполнения основных проверок корректности, таких как проверка типов или значения полей. Однако у нее нет поддержки __post_init__ метода расширенной проверки значений, как это описано в моей предыдущей статье.

from dataclasses import dataclass from omegaconf import MISSING, OmegaConf import hydra from hydra.core.config_store import ConfigStore @dataclass # @dataclass(frozen=True) способ определения полей только для чтения class MySQLConfig: driver: str = "mysql" host: str = "localhost" port: int = 3306 user: str = MISSING password: str = MISSING @dataclass class Config: db: DBConfig = MISSING cs = ConfigStore.instance() cs.store(name="config", node=Config) cs.store(group="db", name="mysql", node=MySQLConfig) @hydra.main(config_path="conf", config_name="config") def my_app(cfg: Config) -> None: print(OmegaConf.to_yaml(cfg)) if __name__ == "__main__": my_app()

Группа конфигураций

Hydra вводит концепцию под названием config group . Идея которой состоит в том, чтобы сгруппировать файлы конфигурации одного типа (или для выполнения одних задач) и затем выбирать один из них во время выполнения приложения. Например, у вас имеется группа настроек «Базы данных» с одной конфигурацией для Postgres, а другой для MySQL.

Когда конфигурация приложения станет более сложной, то в вашей программе она может иметь следующую структуру (пример из документации Hydra).

├── conf │ ├── config.yaml │ ├── db │ │ ├── mysql.yaml │ │ └── postgresql.yaml │ ├── schema │ ├── school.yaml │ ├── support.yaml │ └── warehouse.yaml └── my_app.py

Например, вы хотите протестировать свое приложение с различными комбинациями опций db , schema и ui , это можно сделать следующим образом:

python my_app.py db=postgresql schema=school.yaml

Далее…

Hydra поддерживает использование нескольких наборов параметров конфигурации с опцией —multirun , при этом запускаются параллельно несколько задач с различными файлами конфигурации. Например, для предыдущего примера мы можем запустить скрипт следующим образом:

python my_app.py schema=warehouse,support,school db=mysql,postgresql -m

В этом случае в основном потоке запускаются 6 задач одновременно:

[2019-10-01 14:44:16,254] - Launching 6 jobs locally [2019-10-01 14:44:16,254] - Sweep output dir : multirun/2019-10-01/14-44-16 [2019-10-01 14:44:16,254] - #0 : schema=warehouse db=mysql [2019-10-01 14:44:16,321] - #1 : schema=warehouse db=postgresql [2019-10-01 14:44:16,390] - #2 : schema=support db=mysql [2019-10-01 14:44:16,458] - #3 : schema=support db=postgresql [2019-10-01 14:44:16,527] - #4 : schema=school db=mysql [2019-10-01 14:44:16,602] - #5 : schema=school db=postgresql

Вывод

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

Надеюсь, вам понравится эта статья, не стесняйтесь оставлять свои комментарии ниже.

configparser — Парсер конфигурационного INI файла¶

Модуль предоставляет класс ConfigParser , реализующий базовый язык конфигурации, который предоставляет структуру, аналогичную Microsoft Windows INI файлам. Его можно использовать для написания Python программ, которые могут легко настраиваться конечными пользователями.

Библиотека не интерпретирует и не пишет префиксы тип-значение используемые в Реестре Windows расширяя версию синтаксиса INI.

Модуль shlex Поддержка создания мини-языков, подобных оболочкам Unix, которые можно использовать в качестве альтернативного формата для файлов с конфигурацией приложения. Модуль json Модуль json реализует подмножество синтаксиса JavaScript, которое также может использоваться для этих целей.

Быстрый старт¶

Давайте рассмотрим базовый файл конфигурации, который выглядит следующим образом:

[DEFAULT] ServerAliveInterval = 45 Compression = yes CompressionLevel = 9 ForwardX11 = yes [bitbucket.org] User = hg [topsecret.server.com] Port = 50022 ForwardX11 = no 

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

>>> import configparser >>> config = configparser.ConfigParser() >>> config['DEFAULT'] = 'ServerAliveInterval': '45', . 'Compression': 'yes', . 'CompressionLevel': '9'> >>> config['bitbucket.org'] = <> >>> config['bitbucket.org']['User'] = 'hg' >>> config['topsecret.server.com'] = <> >>> topsecret = config['topsecret.server.com'] >>> topsecret['Port'] = '50022' # мутация парсера >>> topsecret['ForwardX11'] = 'no' # тоже самое >>> config['DEFAULT']['ForwardX11'] = 'yes' >>> with open('example.ini', 'w') as configfile: . config.write(configfile) . 

Как вы видите, мы можем рассматривать configparser так же, как словарь. Есть различия, изложено позже, но поведение очень близкое к тому, что можно ожидать от словаря.

Теперь, когда мы создали и сохранили файл конфигурации, давайте прочитаем его и рассмотрим данные, которые он хранит.

>>> config = configparser.ConfigParser() >>> config.sections() [] >>> config.read('example.ini') ['example.ini'] >>> config.sections() ['bitbucket.org', 'topsecret.server.com'] >>> 'bitbucket.org' in config True >>> 'bytebong.com' in config False >>> config['bitbucket.org']['User'] 'hg' >>> config['DEFAULT']['Compression'] 'yes' >>> topsecret = config['topsecret.server.com'] >>> topsecret['ForwardX11'] 'no' >>> topsecret['Port'] '50022' >>> for key in config['bitbucket.org']: . print(key) user compressionlevel serveraliveinterval compression forwardx11 >>> config['bitbucket.org']['ForwardX11'] 'yes' 

Как мы видим выше, API довольно прост. Малая порция магии включает раздел DEFAULT , который предоставляет значения по умолчанию для всех остальных разделов [1]. Следует также отметить, что ключи в секциях не чувствительны к регистру и хранятся в нижнем регистре [1].

Поддерживаемые типы данных¶

Конфигурационные парсеры не используют типы данных значений в конфигурационных файлах, всегда сохраняя их как строки. Это означает, что если нужны другие типы данных, следует преобразовать их самостоятельно:

>>> int(topsecret['Port']) 50022 >>> float(topsecret['CompressionLevel']) 9.0 

Так как задача общая, парсеры конфигурации предоставляют диапазон удобных методов геттеров, чтобы обращаться с целыми числами, флоатами и логическими типами. Последнее самое интересное, потому что просто передача значения bool() не принесёт пользы, так как bool(‘False’) все ещё True . Поэтому конфигурационные парсеры также предоставляют getboolean() . Метод не учитывает регистр и распознаёт логические значения из ‘yes’ / ‘no’ , ‘on’ / ‘off’ , ‘true’ / ‘false’ и ‘1’ / ‘0’ [1]. Например:

>>> topsecret.getboolean('ForwardX11') False >>> config['bitbucket.org'].getboolean('ForwardX11') True >>> config.getboolean('bitbucket.org', 'Compression') True 

Кроме getboolean() , парсерами конфигурации также предоставляют эквивалентные getint() и getfloat() методы. Можно зарегистрировать собственные конвертеры и настроить предоставленные. [1]

Запасные значения¶

Как и в случае со словарём, можно использовать get() метод раздела для предоставления резервных значений:

>>> topsecret.get('Port') '50022' >>> topsecret.get('CompressionLevel') '9' >>> topsecret.get('Cipher') >>> topsecret.get('Cipher', '3des-cbc') '3des-cbc' 

Обратите внимание, что значения по умолчанию имеют приоритет над резервными значениями. Например, в нашем примере ключ ‘CompressionLevel’ был указан только в разделе ‘DEFAULT’ . Если мы попытаемся получить его из раздела ‘topsecret.server.com’ , мы всегда получим значение по умолчанию, даже если мы зададим резервный вариант:

>>> topsecret.get('CompressionLevel', '3') '9' 

Кроме того, необходимо учитывать, что get() метод на уровне парсера предоставляет пользовательский, более сложный интерфейс, поддерживаемый для обратной совместимости. При использовании этого метода резервное значение может быть предоставлено с помощью аргумента fallback только для ключевых аргументов:

>>> config.get('bitbucket.org', 'монстр', . fallback='Нет таких вещей, как монстры') 'Нет таких вещей, как монстры' 

Один и тот же fallback аргумент может использоваться с методами getint() , getfloat() и getboolean() , например:

>>> 'BatchMode' in topsecret False >>> topsecret.getboolean('BatchMode', fallback=True) True >>> config['DEFAULT']['BatchMode'] = 'no' >>> topsecret.getboolean('BatchMode', fallback=True) False 

Поддерживаемая структура INI файлов¶

Файл конфигурации состоит из возглавляемых заголовком [section] разделов, за которым следуют записи ключа/значения, разделённые определенной строкой ( = или : по умолчанию [1]). По умолчанию имена разделов чувствительны к регистру, но ключи нет [1]. Начальный и конечный пробелы удаляются из ключей и значений. Значения могут быть пропущены, и в этом случае разделитель ключа/значения также может быть оставлен без внимания. Значения также могут охватывать несколько строк при условии, что они являются более глубокими, чем первая строка значения. В зависимости от режима парсера пустые строки могут рассматриваться как части многострочных значений или игнорироваться.

Файлы конфигурации могут содержать комментарии с префиксом из определенных символов ( # и ; по умолчанию [1]). Комментарии могут появляться сами по себе на пустой строке, возможно, с отступом. [1]

[Простые значения] ключ=значение пробелы в ключах=разрешено пробелы в значениях=разрешено также пробелы вокруг разделителя = очевидно вы также можете использовать : чтобы отделить ключи от значений [Все значения являются строками] значения, подобные этому: 1000000 или этому: 3.14159265359 рассматриваются ли они как числа? : нет целые числа, числа с плавающей запятой и логические значения хранятся в виде: строка можно использовать API для прямого получения преобразованных значений: да [Многострочные Значения] припев: Я лесоруб, и со мной всё в порядке Я сплю всю ночь и работаю весь день [Никаких Значений] ключ_без_значения здесь пустое строковое значение = [Вы можете использовать комментарии] # подобный этому ; или этому # По умолчанию только в пустой строке. # Встроенные комментарии могут быть вредными, поскольку они мешают пользователям # использовать разделяющие символы в качестве частей значений. # Тем не менее, это можно настроить. [У Разделов Могут Быть Отступы] могут_ли_значения_быть_такими_же = True означает_ли_это_что-то_особенное = False цель = форматирование для удобства чтения мультистрочные_значения = обрабатываются просто отлично, если у них отступ глубже, чем первая строка значения # Я упоминал, что мы тоже можем делать отступы в комментариях? 

Интерполяция значений¶

Помимо функциональных возможностей ядра, ConfigParser поддерживает интерполяцию. Это означает, что значения могут быть предварительно обработаны перед возвратом их из вызова get() .

class configparser. BasicInterpolation ¶

Реализация по умолчанию, используемая в ConfigParser . Он позволяет создавать значения, содержащие форматные строки, которые ссылаются на другие значения в том же разделе, или значения в специальном разделе по умолчанию [1]. При инициализации могут быть указаны дополнительные значения по умолчанию.

[Paths] home_dir: /Users my_dir: %(home_dir)s/lumberjack my_pictures: %(my_dir)s/Pictures [Escape] gain: 80%% # используйте %%, чтобы экранировать символ % (%- единственный символ, который нужно экранировать) 

В приведенном выше примере ConfigParser с interpolation, установленной в BasicInterpolation() , разрешит %(home_dir)s на значение home_dir ( /Users в данном случае). %(home_dir)s в действительности разрешит /Users/lumberjack . Все интерполяции выполняются по запросу, поэтому ключи, используемый в цепочке ссылок, не должны указываться в определенном порядке в файле конфигурации.

Если для interpolation задано значение None , парсер просто возвращает %(my_dir)s/Pictures как значение my_pictures и %(home_dir)s/lumberjack как значение my_dir .

class configparser. ExtendedInterpolation ¶

Альтернативный обработчик для интерполяции, реализующий более продвинутый синтаксис, используемый в сущности zc.buildout . Расширенная интерполяция использует $ для обозначения значения из внешней секции. Интерполяция может охватывать несколько уровней. Для удобства, если часть section: пропущена, интерполяция по умолчанию соответствует текущему разделу (и, возможно, значениям по умолчанию из специального раздела).

Например, конфигурация, указанная выше с базовой интерполяцией, будет выглядеть следующим образом с расширенной интерполяцией:

[Paths] home_dir: /Users my_dir: $/lumberjack my_pictures: $/Pictures [Escape] cost: $$80 # использовано $$ для экранирования одного $ ($ это единственный символ, который должен быть экранирован) 

Также можно выбрать значения из других разделов:

[Common] home_dir: /Users library_dir: /Library system_dir: /System macports_dir: /opt/local [Frameworks] Python: 3.2 path: $/Library/Frameworks/ [Arthur] nickname: Two Sheds last_name: Jackson my_dir: $/twosheds my_pictures: $/Pictures python_dir: $/Python/Versions/$

Доступ к протоколу отображения¶

Добавлено в версии 3.2.

Доступ к протоколу отображения — это общее имя функциональности, которое позволяет использовать обычные объекты, как если бы они были словарями. В случае configparser реализация интерфейса отображения использует нотацию parser[‘section’][‘option’] .

parser[‘section’] , в частности, возвращает прокси для данных раздела в парсере. Это означает, что значения не копируются, но берутся из исходного парсера по требованию. Ещё важнее то, что при изменении значений в прокси разделе они фактически изменяются в исходном парсере.

configparser объекты ведут себя как можно похоже к реальным словарям. Интерфейс отображения завершен и соответствует MutableMapping ABC. Однако есть несколько различий, которые следует учитывать:

    По умолчанию все ключи в разделах доступны без учёта регистра [1]. Например, for option in parser[«section»] возвращает только optionxform имя ключа опции. Это означает, что по умолчанию используются ключи с нижним регистром. При этом для раздела, содержащего ключ ‘a’ , оба выражения возвращают True :

"a" in parser["section"] "A" in parser["section"] 
  • попытка удалить его вызывает ValueError ,
  • parser.clear() оставляет его нетронутым,
  • parser.popitem() никогда не возвращает его.

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

Настройка поведения анализатора¶

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

Наиболее распространенным способом изменения способа работы конфигурационного парсера является использование опций __init__() :

  • defaults, значение по умолчанию: None Этот параметр принимает словарь пар «ключ-значение», который первоначально помещается в раздел DEFAULT . Это предоставляет элегантный способ поддержки кратких файлов конфигурации, в которых не указаны значения, совпадающие с задокументированными значениями по умолчанию. Подсказка: если вы хотите указать значения по умолчанию для раздела, используйте read_dict() перед чтением фактического файла.
  • dict_type, значение по умолчанию: dict Этот параметр оказывает значительное влияние на поведение протокола отображения и внешний вид записанных файлов конфигурации. Со стандартным словарем каждый раздел хранится в порядке их добавления в парсер. То же самое касается вариантов внутри разделов. Альтернативный тип словаря может быть использован, например, для сортировки разделов и параметров при обратной записи. Обратите внимание: существуют способы добавления набора пар «ключ-значение» в одной операции. При использовании в этих операциях обычного словаря порядок ключей будет упорядочен. Например:

>>> parser = configparser.ConfigParser() >>> parser.read_dict('section1': 'key1': 'value1', . 'key2': 'value2', . 'key3': 'value3'>, . 'section2': 'keyA': 'valueA', . 'keyB': 'valueB', . 'keyC': 'valueC'>, . 'section3': 'foo': 'x', . 'bar': 'y', . 'baz': 'z'> . >) >>> parser.sections() ['section1', 'section2', 'section3'] >>> [option for option in parser['section3']] ['foo', 'bar', 'baz'] 
>>> import configparser >>> sample_config = """ . [mysqld] . user = mysql . pid-file = /var/run/mysqld/mysqld.pid . skip-external-locking . old_passwords = 1 . skip-bdb . # сегодня нам не нужна ACID . skip-innodb . """ >>> config = configparser.ConfigParser(allow_no_value=True) >>> config.read_string(sample_config) >>> # Настройки со значениями обрабатываются как и раньше: >>> config["mysqld"]["user"] 'mysql' >>> # Настройки без значений предоставляют None: >>> config["mysqld"]["skip-bdb"] >>> # Настройки, которые не указаны, все равно вызывают ошибку: >>> config["mysqld"]["does-not-exist"] Traceback (most recent call last): . KeyError: 'does-not-exist' 

Изменено в версии 3.2: В предыдущих версиях configparser поведение соответствовало comment_prefixes=(‘#’,’;’) и inline_comment_prefixes=(‘;’,) .

Обратите внимание на то, что парсеры конфигурации не поддерживают экранирование префиксов комментариев использующих inline_comment_prefixes, может препятствовать тому, чтобы пользователи определяли параметр с символами используемые как префиксы комментария. Если сомневаетесь, избегайте установки inline_comment_prefixes. В любом случае единственным способом сохранения символов префикса комментария в начале строки в многострочных значениях является, например, интерполяция префикса:

>>> from configparser import ConfigParser, ExtendedInterpolation >>> parser = ConfigParser(interpolation=ExtendedInterpolation()) >>> # также можно использовать BasicInterpolation по умолчанию >>> parser.read_string(""" . [DEFAULT] . hash = # . . [hashes] . shebang = . $ !/usr/bin/env python . $  -*- coding: utf-8 -*- . . extensions = . enabled_extension . another_extension . #disabled_by_comment . yet_another_extension . . interpolation not necessary = if # не находится в начале строки . even in multiline values = line #1 . line #2 . line #3 . """) >>> print(parser['hashes']['shebang']) #!/usr/bin/env python # -*- coding: utf-8 -*- >>> print(parser['hashes']['extensions']) enabled_extension another_extension yet_another_extension >>> print(parser['hashes']['interpolation not necessary']) if # не находится в начале строки >>> print(parser['hashes']['even in multiline values']) line #1 line #2 line #3 

Изменено в версии 3.2: В предыдущих версиях configparser поведение соответствовало strict=False .

[Section] key = multiline value with a gotcha this = is still a part of the multiline value of 'key' 

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

По умолчанию, используя getboolean() , парсеры конфигурации считают следующие значения True : ‘1’ , ‘yes’ , ‘true’ , ‘on’ и следующие False значения: ‘0’ , ‘no’ , ‘false’ , ‘off’ . Это можно переопределить, указав пользовательский словарь строк и их логических результатов. Например:

>>> custom = configparser.ConfigParser() >>> custom['section1'] = 'funky': 'nope'> >>> custom['section1'].getboolean('funky') Traceback (most recent call last): . ValueError: Not a boolean: nope >>> custom.BOOLEAN_STATES = 'sure': True, 'nope': False> >>> custom['section1'].getboolean('funky') False 

Другие типичные логические пары включают accept / reject или enabled / disabled .

ConfigParser. optionxform ( option )

Метод преобразует имена параметров для каждой операции чтения, получения или установки. По умолчанию имя преобразуется в нижний регистр. Это также означает, что при записи файла конфигурации все ключи будут в нижнем регистре. Переопределите этот метод, если он не подходит. Например:

>>> config = """ . [Section1] . Key = Value . . [Section2] . AnotherKey = Value . """ >>> typical = configparser.ConfigParser() >>> typical.read_string(config) >>> list(typical['Section1'].keys()) ['key'] >>> list(typical['Section2'].keys()) ['anotherkey'] >>> custom = configparser.RawConfigParser() >>> custom.optionxform = lambda option: option >>> custom.read_string(config) >>> list(custom['Section1'].keys()) ['Key'] >>> list(custom['Section2'].keys()) ['AnotherKey'] 

Функция optionxform преобразует имена опций в каноническую форму. Это должна быть идемпотентная функция: если имя уже в канонической форме, его нужно вернуть без изменений.

ConfigParser. SECTCRE ¶

Скомпилированное регулярное выражение, используемое для парсинга заголовков разделов. По умолчанию [section] совпадает с именем «section» . Пробел считается частью названия раздела, таким образом, [ larch ] будет читаться как раздел имени » larch » . Переопределите этот атрибут, если он не подходит. Например:

>>> import re >>> config = """ . [Section 1] . option = value . . [ Section 2 ] . another = val . """ >>> typical = configparser.ConfigParser() >>> typical.read_string(config) >>> typical.sections() ['Section 1', ' Section 2 '] >>> custom = configparser.ConfigParser() >>> custom.SECTCRE = re.compile(r"\[ *(?P[^]]+?) *\]") >>> custom.read_string(config) >>> custom.sections() ['Section 1', 'Section 2'] 

Хотя объекты ConfigParser также используют атрибут OPTCRE для распознавания строк параметров, переопределять его не рекомендуется, поскольку это будет мешать allow_no_value и delimiters параметрам конструктора.

Устаревшие примеры API¶

В основном из-за проблем обратной совместимостью configparser также предоставляет устаревший API с явными методами get / set . Несмотря на наличие допустимых вариантов использования для рассмотренных ниже методов, для новых проектов предпочтителен доступ через протоколам отображения. Устаревший API в разы более мудрёный, низкоуровневый и нелогичный.

Пример записи в файл конфигурации:

import configparser config = configparser.RawConfigParser() # Обратите внимание, что с помощью функций набора RawConfigParser можно назначить # нестроковые значения для внутренних ключей, но будет получена ошибка, когда # попытка записи в файл или когда вы получаете его в необработанном режиме. Установка # значения, использующие протокол отображения или ConfigParser set(), не позволяют # таким состояться. config.add_section('Section1') config.set('Section1', 'an_int', '15') config.set('Section1', 'a_bool', 'true') config.set('Section1', 'a_float', '3.1415') config.set('Section1', 'baz', 'fun') config.set('Section1', 'bar', 'Python') config.set('Section1', 'foo', '%(bar)s is %(baz)s!') # Запись нашего конфигурационного файла в 'example.cfg' with open('example.cfg', 'w') as configfile: config.write(configfile) 

Пример повторного чтения файла конфигурации:

import configparser config = configparser.RawConfigParser() config.read('example.cfg') # getfloat() вызывает исключение, если значение не является флоат # getint() и getboolean() также сделает это для их соответствующих типов a_float = config.getfloat('Section1', 'a_float') an_int = config.getint('Section1', 'an_int') print(a_float + an_int) # Обратите внимание, что следующий вывод не интерполируется '%(bar)s' или '%(baz)s'. # Это происходит потому, что мы используем RawConfigParser(). if config.getboolean('Section1', 'a_bool'): print(config.get('Section1', 'foo')) 

Для получения интерполяции используйте ConfigParser :

import configparser cfg = configparser.ConfigParser() cfg.read('example.cfg') # Установить опцию *raw* аргумента get() в True если вы хотите отключить # интерполяция в одной операции get. print(cfg.get('Section1', 'foo', raw=False)) # -> "Питон - это весело!" print(cfg.get('Section1', 'foo', raw=True)) # -> "%(bar)s - это %(baz)s!" # Необязательный аргумент *vars* - это dict с членами, которые будут принимать # приоритет в интерполяции. print(cfg.get('Section1', 'foo', vars='bar': 'Documentation', 'baz': 'evil'>)) # Необязательный аргумент *fallback* можно использовать для предоставления резервного значения print(cfg.get('Section1', 'foo')) # -> "Питон - это весело!" print(cfg.get('Section1', 'foo', fallback='Monty is not.')) # -> "Питон - это весело!" print(cfg.get('Section1', 'monster', fallback='Нет таких вещей, как монстры.')) # -> "Нет таких вещей, как монстры." # print(cfg.get('Section1', 'monster')) будет вызвать NoOptionError # но мы также можем использовать: print(cfg.get('Section1', 'monster', fallback=None)) # -> None 

Значения по умолчанию доступны в обоих типах ConfigParsers. Они используются в интерполяциях, если используемые опции не определены в другом месте:

import configparser # Новый экземпляр с 'bar' и 'baz' по умолчанию для 'Life' и 'hard' каждого config = configparser.ConfigParser('bar': 'Life', 'baz': 'hard'>) config.read('example.cfg') print(config.get('Section1', 'foo')) # -> "Питон - это весело!" config.remove_option('Section1', 'bar') config.remove_option('Section1', 'baz') print(config.get('Section1', 'foo')) # -> "Жизнь тяжела!" 

Объекты ConfigParser¶

class configparser. ConfigParser ( defaults=None, dict_type=dict, allow_no_value=False, delimiters=(‘=’, ‘:’), comment_prefixes=(‘#’, ‘;’), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=configparser.DEFAULTSECT, interpolation=BasicInterpolation(), converters=<> ) ¶

Основа парсера конфигурации. При передаче defaults инициализируется в словарь внутренних значений по умолчанию. При передаче dict_type будет использован для создания объектов словаря для списка разделов, параметров раздела и значений по умолчанию.

При передаче delimiters используется как набор подстрок, отделяющих ключи от значений. Когда передаётся comment_prefixes, он будет использован как набор подстрок, префиксов комментариев в других пустых строках. Комментарии могут иметь отступы. При задании inline_comment_prefixes он будет использоваться как набор подстрок префиксов комментариев в непустых строках.

Если strict имеет значение True (по умолчанию), парсер не допускает дубликатов разделов или параметров при чтении из одного источника (файла, строки или словаря), в результате чего возникают DuplicateSectionError или DuplicateOptionError . Если empty_lines_in_values равно False (по умолчанию — True ), каждая пустая строка обозначает конец параметра. В противном случае внутренние пустые строки многострочной опции сохраняются как часть значения. Если allow_no_value равно True (по умолчанию — False ), принимаются опции без значений; для них сохраняется значение None , и они сериализуются без конечного разделителя.

При default_section указывается имя специального раздела, содержащего значения по умолчанию для других разделов и целей интерполяции (обычно именуемое DEFAULT ). Это значение можно получить и изменить во время выполнения с помощью атрибута default_section сущности.

Поведение интерполяции может быть настроено путём предоставления пользовательского обработчика через аргумент interpolation. None может использоваться для полного отключения интерполяции, ExtendedInterpolation() предоставляет более продвинутый вариант, вдохновленный zc.buildout . Подробнее об этой теме читайте в материале специальный раздел документации.

Все имена опций, используемый в интерполяции, будут передаваться через optionxform() метод как и любая другая ссылка на имя опции. Например, используя реализацию optionxform() по умолчанию (которая преобразует имена опций в нижний регистр), значения foo %(bar)s и foo %(BAR)s эквивалентны.

Когда передан converters , то это должен быть словарь, где каждый ключ представляет имя конвертера типа и каждое значение является вызываемым, реализующим преобразование из строки в требуемый тип данных. Каждый конвертер получает свой собственный соответствующий get*() метод объекта парсера и раздела прокси.

Изменено в версии 3.1: по умолчанию dict_type является collections.OrderedDict .

Изменено в версии 3.2: allow_no_value, delimiters, comment_prefixes, strict, empty_lines_in_values, default_section и interpolation были добавлены.

Изменено в версии 3.5: Добавлен аргумент converters.

Изменено в версии 3.7: Аргумент defaults считывается с помощью read_dict() , обеспечивая консистентное поведение парсера: нестроковые ключи и значения неявно преобразуются в строки.

Изменено в версии 3.8: dict_type по умолчанию — dict , так как теперь он сохраняет порядок вставки.

Возвращает словарь, содержащий значения по умолчанию для всего экземпляра.

Возвращает список доступных разделов; раздел по умолчанию не включён в список.

Добавляет в сущность раздел с именем section. Если раздел имени уже существует, вызывается DuplicateSectionError . Если имя раздела по умолчанию передано, то вызывается ValueError . Имя раздела должно быть строкой; если нет, вызывается TypeError .

Изменено в версии 3.2: Нестроковые имена разделов вызывают TypeError .

has_section ( section ) ¶

Указывает, присутствует ли в конфигурации именованный section. Раздел по умолчанию не подтверждается.

Возвращает список параметров, доступных в указанном section.

has_option ( section, option ) ¶

Если данный section существует и содержит данный option, возвращает True ; иначе возвратит False . Если указанный section является None или пустой строкой, предполагается значение DEFAULT.

read ( filenames, encoding=None ) ¶

Попытка чтения и парсинга итерируемых имён файлов с возвращением списка имён файлов, которые были успешно проанализированы.

Если filenames является строкой, объектом bytes или путеподобный объект , он рассматривается как одно имя файла. Если файл с именем в filenames не может быть открыт, этот файл будет пропущен. Она разработана таким образом, чтобы можно было указать повторяемость потенциальных расположений файлов конфигурации (например, текущий каталог, домашний каталог пользователя и некоторый общесистемный каталог), и будут прочитаны все существующие файлы конфигурации в итерации.

Если ни один из именованных файлов не существует, сущность ConfigParser будет содержать пустой набор данных. Приложение, которое требует загрузки начальных значений из файла, должно загрузить требуемый файл или файлы с помощью read_file() перед вызовом read() для любых дополнительных файлов:

import configparser, os config = configparser.ConfigParser() config.read_file(open('defaults.cfg')) config.read(['site.cfg', os.path.expanduser('~/.myapp.cfg')], encoding='cp1250') 

Добавлено в версии 3.2: Параметр encoding. Ранее все файлы читались с использованием кодировки по умолчанию для open() .

Добавлено в версии 3.6.1: Параметр filenames принимает путеподобный объект .

Добавлено в версии 3.7: Параметр filenames принимает объект bytes .

read_file ( f, source=None ) ¶

Чтение и парсинг конфигурации из f, который должны быть итерируемыми строками Юникода (например, файлы, открытые в текстовом режиме).

Необязательный аргумент source указывающий имя считываемого файла. Если не задано и f имеет атрибут name , то это используемый для source; значение по умолчанию — ‘<. >‘ .

Добавлено в версии 3.2: Заменяет readfp() .

read_string ( string, source=» ) ¶

Парсинг конфигурации из строки.

Необязательный аргумент source указывающий контекстное имя переданной строки. Если не указано, используется » . Обычно он должен быть путём к файловой системе или URL-адресу.

Добавлено в версии 3.2.

read_dict ( dictionary, source=» ) ¶

Загрузка конфигурации из любого объекта, предоставляющего метод items() . Ключи — это имена разделов, значения — это словари с ключами и значениями, которые должны присутствовать в разделе. Если тип используемого словаря сохраняет порядок, разделы и их ключи будут добавлены по порядку. Значения автоматически преобразуются в строки.

Необязательный аргумент source указывающий контекстное имя переданного словаря. Если не указано, используется .

Этот метод может использован для копирования состояние между парсерами.

Добавлено в версии 3.2.

get ( section, option, *, raw=False, vars=None [ , fallback ] ) ¶

Получение значения option для именованного section. Если vars передан, то он должен быть словарём. Поиск option выполняется в vars (при наличии), section и DEFAULTSECT в таком порядке. Если ключ не найден и fallback указан, он используемый в качестве резервного значения. None может быть указан как fallback значение.

Все интерполяции ‘%’ расширяются в возвращаемых значениях, если аргумент raw не имеет ложное значение. Значения ключей интерполяции просматриваются таким же образом, как и опция.

Изменено в версии 3.2: Ключевые аргументы raw, vars и fallback только для защиты пользователей от попыток использования третьего аргумента в качестве fallback резерва (особенно при использовании протокола отображения).

getint ( section, option, *, raw=False, vars=None [ , fallback ] ) ¶

Удобный метод, который объединяет option в указанном section в целое число. Описание get() , raw и vars см. в разделе fallback.

Удобный метод, который приводит option в указанном section к числу с плавающей точкой. Описание get() , raw и vars см. в разделе fallback.

Удобный метод, при котором option в указанном section преобразуется в логическое значение. Обратите внимание, что принимаемые значения для выбора ‘1’ , ‘yes’ , ‘true’ и ‘on’ , которые заставляют этот метод возвращать True и ‘0’ , ‘no’ , ‘false’ и ‘off’ , заставляющие его возвращать False . Данные значения строк проверяются без учёта регистра. Любое другое значение приведет к повышению ValueError . Описание get() , raw и vars см. в разделе fallback.

items ( raw=False, vars=None ) ¶ items ( section, raw=False, vars=None )

Если section не задан, вернёт список section_name, section_proxy пар, включая DEFAULTSECT.

В противном случае вернёт список name value пар для параметров в данной section. Необязательные аргументы имеют то же значение, что и для get() метода.

Изменено в версии 3.8: Элементы, присутствующие в vars, больше не отображаются в результате. Предыдущее поведение смешивало фактические опции парсера с переменными, предоставленными для интерполяции.

set ( section, option, value ) ¶

Если данный раздел существует, задаёт для данной опции заданное значение; иначе вызывает NoSectionError . option и value должны быть строками; в противном случае вызывается TypeError .

write ( fileobject, space_around_delimiters=True ) ¶

Записывает представление конфигурации в указанный файловый объект , который должен быть открыт в текстовом режиме (принятие строк). Это представление может быть проанализировано будущим вызовом read() . Если space_around_delimiters равно истине, разделители между ключами и значениями окружены пробелами.

remove_option ( section, option ) ¶

Удаляет указанного option из указанного section. Если раздел не существует, вызывает NoSectionError . Если вариант существовал для удаления, возвращает True ; иначе возвращает False .

Удалить указанную section из конфигурации. Если раздел действительно существовал, возвращает True . В противном случае возвращает False .

Преобразует имя параметра option найденное во входном файле или переданное клиентским кодом в форму, которую необходимо использовать во внутренних структурах. Реализация по умолчанию возвращает нижерасположенную версию option; подклассы могут переопределить это или клиентский код может задать атрибут этого имени для сущности, чтобы повлиять на это поведение.

Вам не нужен к подкласс парсера, чтобы использовать этот метод, вы можете также установить его на случае к функции, которая принимает аргумент строку и возвращает строку. Установка значения «str», например, сделает имена опций чувствительными к регистру:

cfgparser = ConfigParser() cfgparser.optionxform = str 

Обратите внимание, что при чтении файлов конфигурации пробел вокруг имён опций удаляется до вызова optionxform() .

Не рекомендуется, начиная с версии 3.2: Используйте read_file() вместо него.

Изменено в версии 3.2: Теперь readfp() выполняет итерацию на fp вместо вызова fp.readline() .

Для существующего кода, вызывающего readfp() с аргументами, которые не поддерживают итерацию, следующий генератор может быть использован в виде враппера вокруг файлового объекта:

def readline_generator(fp): line = fp.readline() while line: yield line line = fp.readline() 

Вместо parser.readfp(fp) используйте parser.read_file(readline_generator(fp)) .

Максимальная глубина рекурсивной интерполяции для get() , когда у параметра raw ложное значение. Это относится только к интерполяции используемой по умолчанию.

Объекты RawConfigParser¶

class configparser. RawConfigParser ( defaults=None, dict_type=dict, allow_no_value=False, *, delimiters=(‘=’, ‘:’), comment_prefixes=(‘#’, ‘;’), inline_comment_prefixes=None, strict=True, empty_lines_in_values=True, default_section=configparser.DEFAULTSECT [ , interpolation ] ) ¶

Устаревший вариант ConfigParser . По умолчанию интерполяция отключена и позволяет не строковые имена разделов, имена опций и значения с помощью небезопасных методов add_section и set , а также устаревших методов defaults= ключевых аргументов обработки.

Изменено в версии 3.8: dict_type по умолчанию — dict , так как теперь он сохраняет порядок вставки.

Вместо этого рекомендуется использовать ConfigParser , который проверяет типы значений для внутреннего хранения. Если интерполяция не требуется, можно использовать ConfigParser(interpolation=None) .

add_section ( section ) ¶

Добавляет в сущность раздел с именем section. Если раздел с именем уже существует, вызывается DuplicateSectionError . Если передано имя default section, то вызывается ValueError .

Тип section не проверяется, что позволяет пользователям создавать не строковые именованные разделы. Это поведение не поддерживается и может вызвать внутренние ошибки.

Если данный раздел существует, установить для данной опции указанное значение; в противном случае вызывает NoSectionError . В то время как возможно использовать RawConfigParser (или ConfigParser с набором параметров raw с установленным True) для хранения internal не строковых значений, полная функциональность (включая интерполяцию и выходные файлы) может быть достигнута, используя только строковые значения.

Данный метод позволяет пользователям присваивать не строковые значения ключам. Это поведение не поддерживается и приведет к ошибкам при попытке записи в файл или получения файла в режиме, отличном от необработанного. Используйте API протокола сопоставления, которая не позволяет выполнять присваивание для заданного положения.

Исключения¶

exception configparser. Error ¶

Базовый класс для других configparser исключений.

exception configparser. NoSectionError ¶

Исключение, когда указанный раздел не найден.

exception configparser. DuplicateSectionError ¶

Вызывается исключение, если add_section() вызывается с именем раздела, который уже присутствует, или с строгими парсерами, когда раздел встречается более одного раза в одном входном файле, строке или словаре.

Добавлено в версии 3.2: Были добавлены дополнительные атрибуты source и lineno и аргументы __init__() .

exception configparser. DuplicateOptionError ¶

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

exception configparser. NoOptionError ¶

Вызывается исключение, если указанный параметр не найден в указанном разделе.

exception configparser. InterpolationError ¶

Базовый класс для исключений, возникающих при появлении проблем при выполнении интерполяции строки.

exception configparser. InterpolationDepthError ¶

Исключение вызывается, когда интерполяция строки не может быть завершена, поскольку число итераций превышает MAX_INTERPOLATION_DEPTH . Подкласс InterpolationError .

exception configparser. InterpolationMissingOptionError ¶

Исключение вызывается, когда параметр, на который ссылается значение, не существует. Подкласс InterpolationError .

exception configparser. InterpolationSyntaxError ¶

Вызывается исключение когда исходный текст, в который сделаны замены, не соответствует требуемому синтаксису. Подкласс InterpolationError .

exception configparser. MissingSectionHeaderError ¶

Вызывается исключение при попытке парсинга файла, не имеющего заголовочных разделов.

exception configparser. ParsingError ¶

Вызывается исключение при попытке парсинга файла.

Изменено в версии 3.2: Признак filename и аргумент __init__() были переименованы в source для консистентности.

[1] (1, 2, 3, 4, 5, 6, 7, 8, 9, 10) Парсеры конфигурации обеспечивают высокую степень настройки. Если вас интересует изменение поведения, изложенного в ссылке на сноску, обратитесь к разделу Настройка поведения анализатора.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *