Глобальные константы в Django
Всем привет! Перехожу с богомерзкого php на великолепный python. Как следствие осваиваю django 🙂 И вот есть вопрос: хочу хранить путь к каталогу проекта как глобальную переменную (код из файла settings.py)
PROJECT_ROOT = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.normpath(os.path.abspath(PROJECT_ROOT))
При попытке вызвать оную константу из файла app/modules.py получаю:
NameError: name ‘PROJECT_ROOT’ is not defined
Умные дяди, подскажите маленькому: как хранить глобальные константы?
Отслеживать
задан 5 дек 2011 в 11:53
168 2 2 серебряных знака 6 6 бронзовых знаков
код вызова пожалуйста покажите, settings.py — подключен?
5 дек 2011 в 11:56
2 ответа 2
Сортировка: Сброс на вариант по умолчанию
from django.conf.settings import PROJECT_ROOT
Отслеживать
ответ дан 5 дек 2011 в 12:07
3,028 13 13 серебряных знаков 14 14 бронзовых знаков
Модуль config, которого не было? from django.conf import settings
10 апр 2015 в 18:23
С точки зрения безопасности глобальные переменные — плохая идея. В то же время вы всегда можете использовать
from proj_name.settings import your_var
Это позволит разделять пространство имён и использовать удобные для Вас переменные в нужном месте.
Собственный модуль settings
Этот модуль родился в результате переосмысления (или недопонимания) мной вот этого пространного документа: Splitting up the settings file, размещённого на официальном сайте Django.
Постановка задачи
При старте веб-приложения на Django (как посредством запуска отладочного сервера, так и в качестве WSGI-приложения) фреймворк первым делом выполняет модуль, задающий начальные настройки проекта. Источник кода задаётся переменной окружения DJANGO_SETTINGS_MODULE. При создании Django-проекта стандартным способом, например:
$ django-admin startproject myproject
создаётся и модуль настроек. Это файл ‘myproject/myproject/settings.py’. Изменяя и дополняя его, программист настраивает проект, добавляет в него собственные и сторонние компоненты и т. д.
В простых проектах, разрабатываемых одним бэкенд-программистом, бывает вполне разумно ограничиться таким модулем настроек. Однако по мере роста проекта возникают следующие проблемы:
- Настройки проекта для развёртывания в боевой или тестовой среде очень отличаются от настроек, с которыми проект запускают разработчики. Например, в бою приложение требует «большой» SQL-сервер (PostgreSQL или MySQL) и дополнительную базу данных «ключ-значение» для хранения кэшируемых данных (memcached или Redis), в то время как разработчик на своём компютере привык обходиться SQLite. Зато настройки разработчика включают дополнительные модули для отладки проекта (например, debug_toolbar), которые не должны попаcть в production.
- Переменные, устанавливающие режим отладки в Django (DEBUG, TEMPLATE_DEBUG и др.), а также аналогичные переменные для сторонних компонентов, должны быть включены при разработке и выключены в продакшне. За этим довольно муторно следить при коммитах.
- В модуле settings хранятся чувствительные данные (SECRET_KEY, секреты/пароли для аутентификации приложения на различных сервисах и т. д.), которые становится небезопасно хранить в одном репозитории с кодом. Это особенно важно для open source-проектов, а также для крупных проектов, в которых доступ к кодовой базе имеет много разработчиков.
Решение
Мой модуль settings обладает максимальной обратной совместимостью с дефолтовым ‘myproject/myproject/settings.py’: все ссылки на myproject.settings, если они действительно необходимы, остаются в силе. В то же время моё решение позволяет администратору проекта обезопасить приватные данные, а разработчикам − организовать себе наиболее комфортную среду на собственный вкус, независимо от коллег. Дополнительным плюсом является механизм наследования настроек: в локальных настройках можно получить доступ к общим настройкам.
Минус: для хранения локальных настроек нужно придумать какой-то отдельный метод, так как репозиторий использовать не получится. Решение этого вопроса обычно лежит в организационной плоскости: передавать секрет от более опытных коллег менее опытным, публиковать сэмпловый ‘local.py’ в приватном разделе wiki и т. п.
Зато мой метод крайне прост и быстр, не вмешивается в процесс парсинга настроек фреймворком и не создаёт лишних сущностей вроде специальных *.ini/*.conf-файлов с парсерами, классов настроек или модифицирующих настройки функций.
Hands-on
Вот последовательность действий по «апгрейду» классического модуля настроек (подразумевается, что код хранится в git-репозитории):
- Создайте в каталоге главного приложения подкаталог ‘settings’. Путь к нему будет выглядеть так: ‘myproject/myproject/settings/’.
- Переместите ваш старый ‘settings.py’ в созданный в п. 1 каталог ‘myproject/myproject/settings/’ и переименуйте его в ‘common.py’. Будем ссылаться на этот файл в дальнейшем как на «общие настройки».
Если вы используете в проекте относительные пути от файла настроек, увеличьте глубину вложенности на один каталог. Например, код типа:
BASE_DIR = dirname(dirname(abspath(__file__)))
должен превратиться в
BASE_DIR = dirname(dirname(dirname(abspath(__file__))))
from myproject.settings.common import *
Вы можете добавить локальные настройки, начиная со следующей строки.
Например, если вы − разработчик, и хотите использовать замечательный инструмент Django Debug Toolbar, вы можете добавить следующую строку:
INSTALLED_APPS += ('debug_toolbar', )
try: from myproject.settings.local import * except ImportError: from myproject.settings.common import *
Этот вариант рассчитан на тот случай, когда настройки, содержащиеся в common.py, вполне достаточны для того, чтобы проект запустился. Это вряд ли будет соответствовать истине в большом проекте. Как минимум, из общих настроек следует изъять SECRET_KEY по соображениям безопасности.
Если без файла локальной конфигурации запуск проекта не имеет смысла, можно не пытаться обойтись глобальными настройками, а выбросить исключение:
try: from myproject.settings.local import * except ImportError: raise Exception('Please create local.py file')
- велосипедостроение
- django framework
Как получить доступ к значению константы в settings.py
Я хотел бы спросить, как получить доступ к значению константы, объявленной в setting.py следующим образом:
PAYMENT_VARIANTS = < 'redsys': ('payments_redsys.RedsysProvider', < 'merchant_code': '123456789', 'terminal': '1', 'shared_secret': 'aaaaaaaaaaaaaaaaaaaaaaaaaa', 'currency': '978', >)
Я могу импортировать:
from django.conf import settings
Но, как я могу получить доступ к значению поля ‘currency’ или другого на этом уровне.
Спасибо, с уважением,
Вы можете сделать, например, так:
from django.conf import settings if settings.DEBUG: # Do something
В вашем случае это будет что-то вроде:
settings.PAYMENT_VARIANTS['redsys']
Вы можете получить доступ к словарю PAYMENT_VARIANTS, используя settings.PAYMENT_VARIANTS и получить доступ к ключу currency следующим образом
settings.PAYMENT_VARIANTS['redsys'][1]['currency]
Антипаттерн settings.py

Время от времени я сталкиваюсь с паттернами разработки, которые существуют не потому что они хорошо решают какую-то проблему, а потому что так сделано в популярном фреймворке X, следовательно, думают многие — это хорошо.
Сейчас я хочу понегодовать на паттерн «все настройки — в settings.py». Понятно, что популярность он набрал благодаря Django. Я то и дело встречаю в проектах, никак не завязанных на этот фреймворк ту же самую историю: большая кодовая база, маленькие, хорошенькие никак не связанные друг с другом компоненты, и нате вам: все дружно из произвольных мест лезут в волшебный недомодуль settings за своими константами.
Итак, почему же такой подход на мой взгляд отвратителен.
Проблемы с каскадными настройками
В проектах из реальной жизни, как правило, нужно минимум три набора настроек: чтобы погонять проект на localhost, чтобы запускать unittest’ы, и чтобы крутить всё на боевых серверах. При этом большая часть настроек обычно совпадает во всех случаях, а некоторые различаются.
Например, у вас используется MongoDB в качестве хранилища. В общем случае, коннектиться к ней нужно на localhost и использовать DB с именем my_project . Однако для запуска unittest’ов нужно брать DB с другим именем, чтобы не задеть боевые данные: скажем, unittests . А в случае продакшена коннектиться нужно не на localhost, а на вполне определённый IP, на сервер, отданный под монгу.
Так как же, в зависимости от внешних условий settings.MONGODB_ADDRESS из settings.py должна принимать различные значения? Обычно в ход идёт voodoo-конструкция в конце, состоящая из __import__ , __dict__ , vars() , try/except ImportError , которая пытается дополнить и перекрыть пространство имён всеми потрохами другого модуля вроде settings_local.py.
То, что дополнительно нужно подгружать именно _local.py задаётся или хардкодом или через переменную окружения. В любом случае, чтобы те же например unittest’ы включили свои настройки только на время запуска приходится плясать с бубном и нарушать Zen of Python: Explicit is better than implicit.
Кроме того, такое решение сопряжено с другой проблемой, описанной далее.
Исполняемый код
Хранить настройки в виде исполняемого py-кода — жутко. На самом деле весь паттерн, видимо, изначально появился как якобы простое и элегантное решение: «А зачем нам какие-то cfg-парсеры, если можно сделать всё прям на питоне? И возможностей ведь больше!». В сценариях чуть сложнее тривиальных решение оборачивается боком. Рассмотрим, например, такой сниппет:
# settings.py BASE_PATH = os.path.dirname(__file__) PROJECT_HOSTNAME = 'localhost' SOME_JOB_COMMAND = '%s/bin/do_job.py -H %s' % (BASE_PATH, PROJECT_HOSTNAME) # settings_production.py PROJECT_HOSTNAME = 'my-project.ru'
Понимаете в чём проблема? То, что мы перекрыли значение PROJECT_HOSTNAME абсолютно по барабану для итогового значения SOME_JOB_COMMAND . Мы могли бы скрипя зубами скопипастить определение SOME_JOB_COMMAND после перекрытия, но даже это не возможно: BASE_PATH то, в другом модуле. Копипастить и его? Не слишком ли?
Я уже не говорю о том, что исполняемый код в качестве конфигурации может просто приводить к трудноотлаживаемым ImportError при старте приложения в новой среде.
Поэтому я уверен, что мухи должны быть отдельно, котлеты отдельно: базовые значения в текстовом файле, вычисляемые — в py-модуле.
High-coupling
Хороший проект — тот проект, который возможно разобрать на маленькие кубики, а каждый кубик выложить на github в качестве полноценного open-source проекта.
Когда всё так и есть, но с одним НО: «будьте добры иметь settings.py в корне проекта и чтобы в нём были настройки FOO_BAR , FOO_BAZ и FOO_QUX » выглядит это как-то нелепо, не правда ли? А когда что-то звучит нелепо, обычно это означает, что есть ситуации в которых эта нелепость аукается.
В нашем случае, пример не заставляет долго себя выдумывать. Пусть наше приложение работает с VKontakte API, и у нас есть нечто вроде VKontakteProfileCache , которое в лоб пользуется settings.VK_API_KEY и settings.VK_API_SECRET . Ну пользуется и пользуется, а потом раз, и наш проект должен начать работать сразу с несколькими VKontakte-приложениями. А всё, VKontakteProfileCache спроектирован так, что он работает только с одной парой credentials.
Поэтому стройнее и целесообразнее вообще никогда не обращаться к модулю настроек напрямую. Пусть все потребители принимают нужные настройки через параметры конструктора, через dependency injection framework, как угодно, но не напрямую. А конкретные настройки пусть вытягивает самый-самый нижний уровень вроде кода в if __name__ == ‘__main__’ . А откуда уж он их возьмёт — его личные проблемы. При таком подходе также крайне упрощается unit-тестирование: с какими настройками нужно прогнать, с теми и создаём.
Возможное решение
Итак, паттерн «settings.py» грязью я полил. Мне полегчало, спасибо. Теперь о возможном решении. Я использовал подобный подход в нескольких проектах и нахожу его удобным и лишённым перечисленных проблем.
Настройки храним в текстовых ini-style файлах. Для парсинга используем ConfigObj: он имеет более богатые возможности по сравнению со стандартным ConfigParser, в частности с ним очень просто делать каскады.
В проекте заводим базовый файл настроек default_settings.cfg со всеми возможными настройками и их значениями с разумным умолчанием.
Создаём модуль utils.config с функциями вроде configure_from_files() , configure_for_unittests() , которые возвращают объект с настройками под разные ситуации. configure_from_files() организовывает каскадный поиск по файлам: default_settings.cfg , ~/.my-project.cfg , /etc/my-project.cfg и, вероятно, где-то ещё. Всё зависит от проекта.
Вычисляемые настройки эвалюируются последним шагом сборки объекта-конфигурации.
Самим модулем пользуются лишь запускалки процессов или тестов. Все заинтересованные в настройках классы получают уже готовые значения через инъекции, то есть не общаются с настройками напрямую. На самом деле это не всегда удобно, когда настроек тьма всё же лучше передавать объект-конфигурацию целиком, но это не отменяет того, что её нужно передавать — никакого обращения в лоб.
Быть может, я слишком много понаписал о таком «пустяке» как настройки. Но если хоть кто-нибудь после прочтения задумается перед слепым копированием далеко не совершенного подхода к чему-либо и сделает лучше, проще, веселее, буду считать миссию этого поста выполненной.
- архитектура
- django
- паттерны проектирования
- dependency injection
- конфигурация