Антипаттерн 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 и, вероятно, где-то ещё. Всё зависит от проекта.

    Вычисляемые настройки эвалюируются последним шагом сборки объекта-конфигурации.

    Самим модулем пользуются лишь запускалки процессов или тестов. Все заинтересованные в настройках классы получают уже готовые значения через инъекции, то есть не общаются с настройками напрямую. На самом деле это не всегда удобно, когда настроек тьма всё же лучше передавать объект-конфигурацию целиком, но это не отменяет того, что её нужно передавать — никакого обращения в лоб.

    Быть может, я слишком много понаписал о таком «пустяке» как настройки. Но если хоть кто-нибудь после прочтения задумается перед слепым копированием далеко не совершенного подхода к чему-либо и сделает лучше, проще, веселее, буду считать миссию этого поста выполненной.
    Метки:
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 111
    • +3
      Я все ждал пока ты проведешь аналогию между settins.py и синглтоном и из нее выведешь гуся почему это все плохо, но так тоже хорошо получилось.

      Только про инъекции без примера будет понятно только для тех, кто уже в теме
      • +4
        Да, примеров добавь, плз.
      • +13
        Яблоко шикарное.
        • +3
          Андрей Светлов буквально вчера написал статейку на близкую тему.
          • 0
            Да, любопытно, спасибо. На самом деле для меня flask добавил масла в огонь: грамотно спроектированный фреймворк, по моему мнению, имеет таки несколько сомнительных решений. И тема с настройками — одна из них.
            • +1
              Flask прекрасен тем, что там всё упрощено до предела. На самом деле добавить в него свой формат конфига не сложно. Например, пробовали сделать конфиг на YAML, но оно не прижилось.
              На самом деле, для небольшого проекта можно и обычным settings.py обойтись, а для чего-то большего стоит сразу брать другой фреймвок. Сегодня я лучше Pyramid ничего не видел, его и советую.
          • –1
            Вообще, мне нравится подход Pyramid и, если точнее, PasteScripts. Просто и без излишеств, а главное, что работает. Добавить к этому возможность указания конфига напрямую в uWSGI и сразу понимаешь, что такое счастье и с чем его едят.
            • +3
              Конфиг из PasteDeploy. И внутри это просто жуткая мешанина. INI — ужасный формат. Paste предлагает в качестве решения пользоваться их хуками из paste.deploy.converters, что превращает процесс конфигурирования более-менее сложного проекта в уродский набор инструкций на уровне python-кода.
              • 0
                Может быть. Пока я с этим не столкнулся.
                Касательно INI, то в PasteDeploy он не совсем канонический и вполне удобный. Главное поддерживать разработческий и продакшен (ну и все остальные) в адекватном состоянии, не так, что разработческий правим, а «продакшен будем править, когда деплоиться будем».
            • –5
              Первая мысль, которая пришла мне в голову при взгляде на заголовок — «так вроде ж доменная зона.РФ, да и домен на латинице».
              • НЛО прилетело и опубликовало эту надпись здесь
                • +1
                  Про количество параметров — всецело согласен. Когда их становится несколько и они постоянно тандемом протягиваются по цепочке делегирования, проще передавать конфиг-объект. Но против статического доступа — протестую! :) Как, например, решить в этом случае проблему с `VKontakteProfileCache`?
                  • НЛО прилетело и опубликовало эту надпись здесь
                  • 0
                    > Мотивирует он это тем, что параметры мешают восприятию кода «как газетной статьи».
                    Спорная ценность. Если я не ошибаюсь, кто-то из известных разработчиков старины (Вирт?) вообще был против того, чтоб язык программирования был похож на натуральный язык. Код — это всё-таки далеко не газетная статья. И читают его совсем для других целей.
                    • 0
                      И в тоже время:
                      >>> import this
                      ...
                      Readability counts.
                      ...

                      Это не из старины, но современность.

                      Касательно аргументов функций, то при их большом количестве, лучше использовать именованные аргументы и тогда всё будет неплохо читаться. Но я согласен с тем, что а большинстве случаев лучше подумать о том, как можно уменьшить их число.
                      • 0
                        Несомненно. Большое количество аргументов подразумевает большой объём операций, а функция должна по возможности быть очевидной и делать что-то одно. Поэтому уменьшение количества аргументов — это концептуально правильно. Но всё должно быть в меру, конечно же.
                      • НЛО прилетело и опубликовало эту надпись здесь
                        • 0
                          Да, спасибо. Я имел в виду, что избегать их нужно только для того, чтоб сделать код понятнее. Это ни в коем случае не должно быть самоцелью.
                      • 0
                        Количество параметров само по себе не так важно, гораздо важнее интуитивное их восприятие, ведь к примеру код:

                        def square(x1, y1, x2, y2, x3, y3):
                        ....


                        читается не сложнее чем с двумя.
                        • НЛО прилетело и опубликовало эту надпись здесь
                      • 0
                        Настройки храним в текстовых ini-style файлах.

                        settings.py это конечно не айс, и вы правильно всё написали, но неужели у вас не вызывает это наследие начала 90х тотального ужаса? Я недавно решил попробовать что-то кроме джанги, сел за Pyramid — ini файлы конечно были не самым страшных в этом фреймворке, но красоты точно не добавляли.
                        • +2
                          Если INI ассоциируется с девяностыми, а девяностые ассоциируются с буэ, не проблема: берите YAML, берите XML (буэ!), берите CSV. Основная мысль не в том, что ini рулит, а в том, что рулит неисполняемый текстовый формат.

                          Хотя справедливости ради замечу, что INI очень даже удобен для решения большинства задач.
                          • +1
                            Согласен: меньше предубеждений == меньше проблем.
                            INI за глаза хватает в подавляющем большенстве случаев. Там, где не хватает, сначала надо подумать о том, всё ли вы правильно делаете и, если не найдёте как сделать так, чтобы хватило, искать другой формат.

                            Но вот какой?
                            — XML крут, но не читабелен совершенно, а конфиг должен быть читаемым.
                            — JSON достаточно не плох, но тоже плохо читается, хотя и куда приятнее XML.
                            — YAML прекрасен, но бОльшая часть его плюшек не будет использоваться. Да и ошибиться в его довольно строгом синтаксисе не сложно: не превратится ли процесс разработки в поиск ошибке в YAML?
                            — Питон… Эта статья о том, что он не очень подходит на роль конфига. Всё-таки он для другого создавался и у меня есть опыт просто дико бредового способа его использования в крупном проекте на джанге — совсем не гуд.

                            Что я забыл?

                            INI прост, всем известен, прекрасно воспринимается глазами и даёт один уровень иерархии, которого обычно вполне хватает. Недостаёт, наверное, только чего-нить вроде include (не import!), но, в общем-то, и без него можно прекрасно обойтись.
                            • 0
                              XML хоть и не читабаелен (хотя это спороно, ну да ладно), но обладает одной важной особенностью — его можно провалидировать с использованием схемы, а это очень удобно, я считаю.
                              • 0
                                Да, это плюс. Но, наверное, единственный и в данном случае не очень большой.
                                ИМХО, валидировать конфиг не нужно. Это совсем лишнее.
                                Тем более что, вы для каждого конфига будете делать схему? Заносить в неё каждую опцию? Ну хорошо, можно сделать общую схему, согласен, но всё равно как-то это глупо и не имеет реального смысла…
                              • 0
                                ini при желании дает сколько угодно уровней иерархии (и кстати довольно удобно). В php'шном ZF например:
                                [global]
                                param = value
                                group.param1=value1
                                group.param2=value2
                            • 0
                              На что бы вы их заменили будь ваша воля?
                            • +7
                              Вы серьезно?
                              • +9
                                Да не, чихнул дюжиной абзацев нечаянно
                              • 0
                                Приведите, пожалуйста, в качестве примера, решение проблемы с SOME_JOB_COMMAND предложенным вами способом.
                                • 0
                                  Процитирую автора статьи:

                                  > Поэтому я уверен, что мухи должны быть отдельно, котлеты отдельно:
                                  > базовые значения в текстовом файле, вычисляемые — в py-модуле.

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

                                  Иначе, если делать красиво, то придётся писать свой формат конфига с возможностью вставки в него кусков кода, которые будут последовательно выполнятся после полного прочтения конфига, в известном окружении. Идея интересная, но я уже вижу ряд сложностей.
                                  • 0
                                    Собственно тут всё описано. Берём BASE_PATH и PROJECT_HOSTNAME из текстового конфига и вычисляем SOME_JOB_COMMAND в коде программы.

                                    Это я понял. Я не понимаю в чем профит от переноса базовых параметров в текстовые файлы.
                                    • –2
                                      Профит в отделении мух от коров. Кода от дефайнов.
                                      Это как отделение контроллера от отображения.
                                      Большая логичность кода.
                                      • +5
                                        ок, отделяем мух от котлет:
                                        # settings.py
                                        from constants import *
                                        
                                        # а тут все вычисления
                                        


                                        отделили? отделили.

                                        или так:
                                        # КОНСТАНТЫ
                                        ...
                                        # ВЫЧИСЛЯЕМЫЕ ЗНАЧЕНИЯ
                                        ...
                                        


                                        отделили? отделили. Зачем огород-то городить?

                                        А если нужен set в качестве константы? А если нужен callable в качестве константы? С ini-файлами мы в пролете. Нужно всегда будет иметь в виду десериализацию, там свои тонкости могут быть.
                                        • 0
                                          > А если нужен set в качестве константы? А если нужен callable в качестве константы?
                                          Вот этого не должно быть в нормальном конфиге. Максимум — лист (ini умеет). Всё остальное уже дикий оверхед. Нормальный конфиг должен быть понятен человеку, не знающему питон.

                                          На счёт импорта модуля с конфигом, читайте статью Андрея Светлова, которую я привёл выше.
                                          • +4
                                            Нормальный конфиг должен быть понятен человеку, не знающему питон.

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

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

                                            settings.py — не для этого, он для того, чтобы связать части проекта воедино, без знания программирования там делать нечего. Это ж фреймворк, а не CMS. Те же настройки INSTALLED_APPS для пользователя коробочного продукта никакого смысла не несут, их не нужно ему показывать.
                                            • –2
                                              Мы говорили про конфиги вообще, а скатились к джанге. У кого что болит… Ладно, молчу.
                                              Всё-таки это неправильный подход. Вот вы же используете язык шаблонов (ну там jinja...)? А почему просто питоньими шаблонами не строите html? Вот и тут напрашивается свой язык. Язык конфигов. И это правильно.
                                              А максимум гибкости тут не нужен. А то до абсурда доходит.

                                              P.S. Концепцию INSTALLED_APPS вообще в моём присутствии лучше не упоминать.
                                              • +4
                                                Пользуюсь шаблонизаторами, т.к. удобнее.

                                                А FOO = 123 пишется одинаково что в ini, что в .py.

                                                Насчет максимума гибкости. Понятно, что везде какой-то баланс ищется. Но вот (imho) абстракция ini-файлов для настройки фреймворка имеет все-таки степень дырявости больше необходимой для практической работы. И она не решает ни одну проблему, кроме «настройки фреймворка непрограммистом» при том, что непрограммист и не должен настраивать фреймворк.
                                                • 0
                                                  «Степень дырявости»? Очень интересное словосочетание… Как раз наоборот, мне кажется.
                                                  А с помощью ini (пойду на это утрирование) решается ещё и проблема кривых (шаловливых?) рук.
                                                  • +2
                                                    Проблема кривых рук техническими средствами нерешаема) Тут задачка-то на присвоение переменных и пару импортов максимум.

                                                    Я думаю, что понимаю, о чем речь: о том, что API не должен стимулировать неправильные решения. В случае с settings.py API позволяет сделать все неправильно, но уж не стимулирует. В награду за это мы получаем возможность делать не так, как кажется «правильно» авторам фреймворка.
                                                    • 0
                                                      В общем случае ты меня вроде понял.
                                                      А вот за «делать не так, как кажется «правильно» авторам фреймворка» я бы вывел тебя в чистое поле, поставил лицом к стене и пустил пулю в лоб двумя очередями… Чтоб всю жизнь помнил… Если тебя не устраивает подход авторов фреймвока, то бери другой фреймвок, где таких разногласий нет.
                                                      Вот немного утрированный пример… Питон со стандартными либами, это большой метафреймвок для написания программ. У него есть, например, мой любимый pep8, нарушать который можно, но это будет означать то, что любой человек, который будет разбирать твою программу, будет в замешательстве и недоумении. Или, ещё пример, Zen of Python… Питон, это фреймвок, в котором шаг в сторону карается не соответствием общепринятым нормам.

                                                      Давай закончим спор, так как он бесперспективен. Резюмируя свою точку зрения, хочу сказать, что лет пять-шесть назад, когда я впервые столкнулся с идеей хранить настройки в модуле на питоне, мне это очень понравилось. Но за годы использования и пришёл к выводу, что это не правильно, не удобно и приводит к разным казусам. Некоторые из них решает подход Андрея Светлова, но всё равно мне не нравится.

                                                      P.S. Если наберётся человека три-четыре, разделяющих мою точку зрения, то можем обсудить формат идеального конфига и возможность реализации парсера. А то мысли странные в голове крутятся, но на хабре им не место.
                                      • 0
                                        Профитов несколько. Хотя бы: простые каскады, отсутствие соблазна определить настройку «неправильно», более внятные ошибки нежели ImportError
                                        • 0
                                          простые каскады, отсутствие соблазна определить настройку «неправильно»

                                          Вы бы могли привести примеры?
                                          более внятные ошибки нежели ImportError

                                          Чем невнятен ImportError?
                                          • +4
                                            Для поддержки «каскадов» нужно написать код. Ничего не мешает этот код написать и вызывать из settings.py, если «каскады» нужны. Вот какая разница, текстовые файлы или не текстовые файлы, код все равно нужно написать и вызвать.

                                            Насчет соблазна. В крайнем случае написать комментарий, где что помещать, неужто такая большая проблема? settings.py — это же не сторонний код, в который не влезешь, чтоб что-то поправить, а код проекта, который пишешь, как хочешь и правишь в любой момент.

                                            Насчет ImportError — не согласен. Откуда более внятным ошибкам взяться-то? Если ошибка синтаксическая (неправильно код на python написан), то будет обычный трейс, куда уж внятнее.

                                            А если ошибка в значении переменной, то сообщение об ошибке зависит от вызывающего кода. А там уже без разницы, откуда значение получено, и сообщение зависит от этого вызывающего кода, а не от того, где настройка хранится.
                                      • +2
                                        Проблемы нет. Есть «не подумали». Если подумать, то PROJECT_HOSTNAME и SOME_JOB_COMMAND не должно быть в «settings.py».

                                        У меня в «settings.py» присутствует только глобальная конфигурация, которая влияет на все окружение в целом, и не зависит от окружающей среды. Остальные же настройки в обязательном, подключаемом в «settings.py», файле «settings_local.py», который на каждом сервере и у каждого разработчика на машине — свой. И если разработчик или админ не создаст его, или не укажет там необходимой настройки — при попытке запуска будет выдана вполне определенная ошибка.

                                        Тот факт, что «settings.py» — это исполняемый Python-файл, дает вам огромные возможности и преимущества, и всё, что вам остается, как грамотному разработчику, с умом их использовать.
                                        • 0
                                          Вы немножко не о том. Вы говорите о локальный настройках окружения, а в топике — о вычислении настроек. Скажем, есть у вас следующее:

                                          settings.py
                                          DEBUG = False
                                          VAR = some_func(DEBUG)


                                          settings_local.py
                                          DEBUG = True

                                          DEBUG из settings_local.py переопределяет переменную DEBUG из главного settings.py. Тут то и начинается веселуха…
                                          Где-то ниже строки DEBUG = False в settings.py есть вычисляемая переменная VAR, которая зависит от переменной DEBUG, но после переопределения DEBUG ваша переменная VAR останется неизменной, так как она использует еще непереопределенный DEBUG.
                                          • 0
                                            Вычисляйте VAR после импорта settings_local.py
                                            • 0
                                              Усложняем задачку:

                                              settings.py
                                              DEBUG = False
                                              try:
                                                  from settings_local import *
                                              except ImportError:
                                                  pass
                                              VAR = some_func(DEBUG)
                                              BAR = some_func1(VAR)


                                              settings_local.py
                                              DEBUG = True
                                              VAR = True


                                              С переменной VAR вроде бы справились.
                                              Как же быть тогда с переменной BAR?
                                              • +2
                                                А теперь решение на ini файлах, пожалуйста. =) С ini файлами у вас даже вопроса такого не возникнет, ибо там с BAR просто никак. =)
                                                • 0
                                                  Уффф… я хоть слово об ini сказал?)
                                                  • 0
                                                    Лично вы — нет. Но, а вы как предлагает решать?

                                                    Кстати, я так и не смог понять даже, как же вы с VAR справились. В приведенном фрагменте наличие VAR = True в settings_local.py — абсолютно бессмысленно.

                                                    Уж тогда бы писали что-нибудь вроде:
                                                    try:
                                                        VAR
                                                    except NameError:
                                                        VAR = some_func(DEBUG))

                                                    • 0
                                                      Мдя… Очень эффектно… Ни за что бы не догадался до такого…
                                                      Скорее так:

                                                      if 'VAR' not in locals():
                                                      ...


                                                      Ну ладно, это уже не в тему поста.
                                                      • 0
                                                        Сорри, не увидел следующий пост. :-)
                                                    • 0
                                                      Ну ладно, если
                                                      VAR = some_func(DEBUG)
                                                      

                                                      заменить на
                                                      if 'VAR' not in locals():
                                                          VAR = some_func(DEBUG)
                                                      

                                                      Вас это устроит?

                                                      P.S. Ну и в самом деле, почему в роли оправдывающихся находятся люди, предполагающие, что предложение автора не дает никакого профита? Пускай автор доказывает свою точку зрения и предлагает элегантные решения данных use case'ов
                                                    • 0
                                                      Да просто всё:

                                                      def configure_from_files():
                                                           # la la la
                                                           return eval_values(result)
                                                      
                                                      def eval_values(conf):
                                                           conf['VAR'] = conf['VAR'] or some_func(conf['DEBUG'])
                                                           conf['BAR'] = conf['BAR'] or some_func1(conf['VAR'])
                                                      


                                                      Параноики в этом случае ещё и eval_values протестировать в изоляции смогут.
                                                      • 0
                                                        Да я ж не спорю, что всё просто. Я только за, и за то, что всё просто уже при существующем подходе, с settings.py, никто не мешает такое написать, если оно вам реально нужно.
                                                • 0
                                                  Нет, я понял. И моя мысль была такова: есть локальные настройки, есть глобальные. И они никак не смешиваются. Если DEBUG — это локальная настройка, то ей в принципе нечего делать в
                                                  settings.py, ни одна глобальная настройка не должна переопределяться в локальном файле настроек, и в то же время в глобальном файле, не место локальным настройкам.

                                                  Локальная настройка может вычисляться на основе глобальной, но не наоборот.
                                                  • +2
                                                    Мне предмет спора немножко напоминает «использовать в PHP шаблонизаторы или не использовать».

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

                                                    Я, всё же, за думающего разработчика и гибкие возможности, нежели чем «глупый» разработчик и технологические ограничения.
                                                    • 0
                                                      Мне тоже думающие разработчики нравятся больше глупых разработчиков. Но лучше один раз сделать что-то, что застрахует от глупых (например, потребители вашего продукта если он open-source) и не будет напрягать умных каждый раз по мелачам: им и так есть о чём подумать.
                                                      • +1
                                                        Ну django — это не готовый продукт все-таки, а фреймворк, для, я надеюсь, умных разработчиков. =)

                                                        А в конечном продукте вы можете сделать хоть ini-файлы, хоть вообще веб-интерфейс настройки.
                                                        • +3
                                                          Кстати, в дев-листе джанги постоянно раньше предложения были — давайте конфиг на YAML/ini сделаем, давайте еще как-то, давайте из коробки settings_local сделаем, давайте-давайте. Ответ был в духе: выучите наконец питон, ребята.

                                                          Много людей приходит в джангу, не зная питона, и квест с организацией settings.py — один из шагов к тому, чтобы перестать воспринимать фреймворк как черный ящик, который нужно как-то настроить и что-то там волшебно заработает. Это же фреймворк, а не CMS.
                                                          • –3
                                                            И я показал, как некоторые, кстати очень не дурные, разработчики этот квест решают. Уж лучше сразу ограничиться.
                                                            • +1
                                                              По ссылке решают не этот квест, а какие-то неявные хотенья, не сформулировав даже внятно исходную задачу (которую, скорее всего, можно проще решить). Недурный питонист Александр Кошелев там все правильно написал. За три года ни разу ничего подобного не потребовалось.
                                                              • –4
                                                                Мои фразы читал-то? Я согласен, что это бред и абсурд.
                                                                Но именно до этого часто доводит избыточная гибкость.
                                                                И поддерживать этот продукт я и врагу не пожелаю…
                                                                • +2
                                                                  Т.е. «очень не дурные разработчики» написали продукт, который «поддерживать и врагу не пожелаешь», наворотя, кроме всего прочего, еще и всякой ерунды в settings.py. Ага, замечательные разработчики. Такое, кстати, бывает, когда в питон с других языков переходишь, — тоже в первое время часто писал дико сложную непитонью хрень, болезнь называется «горе от ума».
                                                                  • 0
                                                                    Не цепляйся к словам. Это хорошие программисты, написавшие неплохой продукт. Просто есть несколько очень убийствинных загибонов и некая негибкость ума, не позволяющая от них отказаться, так как это будет означать признание своей неправоты в вопросе, в котором было пролито много крови. Это больше личное.
                                                                    Ну ещё есть нежелание переделывать устоявшуюся схему, на которой работает уже минимум три проекта (может больше, я не в курсе).

                                                                    На счёт перехода с других языков, то это не совсем тот случай.
                                                                    • +1
                                                                      Ок, ситуацию вроде понял. Но какое, в таком случае, это все имеет отношение к рассматриваемому вопросу? Код написан, он работает, в нем сложно разобраться, переписывать его не хотят по личным причинам, причем тут settings.py?

                                                                      Вот я по личным причинам стану в ini-файлах строки заключать в кавычки и сделаю наследника от ConfigObj, который будет уметь их разбирать такими. Придет следующий разработчик и ничего не поймет. ini-файлы — зло, они позволили мне это сделать?
                                                                      • –2
                                                                        Я показал тебе пример того, как оно было написано. Мне кажется, что права на жизнь такое решение не имеет. А с ini такой бред невозможен.
                                                                        Проблема с кавычками, на самом деле, не проблема, так не стоит изобретать велосипедов, когда парсер ini уже давно написан. Бери, например, тот что в PasteDeploy и пусть остальные, поддерживающие твой продукт, читают соответствующую доку по правилам написания этих ini. Или, что скорее, не читают, а просто правят так, как формат прост как три копейки.

                                                                        P.S. Юзайте в джанге settings.py, потому что там так принято и локально менять это не стоит. Просто если будете изобретать свой фреймвок, подумайте о том, как лучше сделать.
                                                  • 0
                                                    столкнулся с аналогичной чехардой, когда попытался организовать settings как отдельный модуль, разнеся различные секции настроек в отдельные .py файлы .(
                                              • +4
                                                Как бы залог хорошей статьи — это для начала проанализировать существующие решения, если они интересны — рассказать о них, на забыв при этом заглянуть в документацию и гайды на официальном сайте.

                                                Из непосредственно решений щупал:
                                                django-config правда там есть свои проблемки с переписыванием manage.py и config.py
                                                А так же django-configurator не решает части описанных проблем, зато предоставляет удобный модульный конфиг для приложений.

                                                Подробный гайд с рассмотрением проблемы:
                                                code.djangoproject.com/wiki/SplitSettings
                                                • 0
                                                  Очень в тему ссылочка. Спасибо! Действительно, стоило её разобрать.
                                                  • 0
                                                    Хотя, наверное, стоило еще добавить, что я от этих штук отказался, чтобы не плодить сущности и 3-rd party и остановился на маленьком самописном меинтере и четырех конфиг-файлах, один — общие настройки, три других — конфиги под разработку, тестирование и продакшен.

                                                    Но если продакшен горизонтально масштабируется более, чем на один сервер, то однозначно стоит использовать django-config, хотя на том этапе, когда под логику перестает хватать одного сервера, обычно уже можно позволить себе написать продвинутый деплоймент.
                                                • 0
                                                  нет
                                                  • +1
                                                    Понимаете в чём проблема? То, что мы перекрыли значение PROJECT_HOSTNAME абсолютно по барабану для итогового значения SOME_JOB_COMMAND.

                                                    В предложенном вами способе SOME_JOB_COMMAND все равно останется в выполняемом коде и поэтому данную константу невозможно будет переопределить. В таком случае, если условиться, что SOME_JOB_COMMAND не подлежит переопределению, то данная проблема легко решается в рамках settings.py:
                                                    # settings.py
                                                    
                                                    BASE_PATH = os.path.dirname(__file__)
                                                    PROJECT_HOSTNAME = 'localhost'
                                                    
                                                    try:
                                                        from settings_local import *
                                                    except ImportError:
                                                        pass
                                                    
                                                    SOME_JOB_COMMAND = '%s/bin/do_job.py -H %s' % (BASE_PATH, PROJECT_HOSTNAME)
                                                    


                                                    Мы могли бы скрипя зубами скопипастить определение SOME_JOB_COMMAND после перекрытия, но даже это не возможно: BASE_PATH то, в другом модуле.

                                                    # settings_local.py
                                                    from settings import *
                                                    
                                                    • –1
                                                      # settings_local.py
                                                      from settings import *


                                                      Перенесётся значение переменной, а не формула его вычисления из имеющихся, уже изменённых, компонентов. Поэтому придётся переносить всё определение. И в этом проблема.
                                                      • +1
                                                        В данном случае автор сетовал именно на отсутствие BASE_PATH. По поводу вычисления SOME_JOB_COMMAND на основании переопределенных параметров посмотрите на первый кусок кода в моем комментарии выше.
                                                        • +1
                                                          Угу… А если у нас SOME_JOB_COMMAND переопределён в settings_local? Или принять эту переменную, как железно вычисляемую, без возможности переопределения алгоритма вычисления?
                                                          Скорее всего это будет правильным решением. Но, к сожалению, не всегда.

                                                          Кстати, в вашем примере хорошо показано разделение между дефайнами и вычислением: то: что до импорта опеделяется, то, что после — вычисляется. В общем-то об этом автор и говорил.
                                                          • 0
                                                            Угу… А если у нас SOME_JOB_COMMAND переопределён в settings_local? Или принять эту переменную, как железно вычисляемую, без возможности переопределения алгоритма вычисления?

                                                            Ну так автор же и предлагает захардкодить алгоритм вычисляемых параметров в исполняемый код, а в конфигах оставить только базовые параметры. Разве нет? Ну если уж вас так беспокоит эта проблема, то ее можно решить так:
                                                            # settings.py
                                                            
                                                            BASE_PATH = os.path.dirname(__file__)
                                                            PROJECT_HOSTNAME = 'localhost'
                                                            
                                                            try:
                                                                from settings_local import *
                                                            except ImportError:
                                                                pass
                                                            
                                                            if 'SOME_JOB_COMMAND' not in locals():
                                                                SOME_JOB_COMMAND = '%s/bin/do_job.py -H %s' % (BASE_PATH, PROJECT_HOSTNAME)
                                                            
                                                            • –1
                                                              Ох что-то мне это напоминает…
                                                              Всё-таки я за хардкод таких вещей. Но дефайны лучше уж держать на языке, для этого предназначенном, никак не на питоне. В конце концов это даст возможность использовать конфиг людьми, не знающими языка.
                                                              • +1
                                                                Вы, по-моему, уже сами не понимаете чего хотите.
                                                                В конце концов это даст возможность использовать конфиг людьми, не знающими языка.

                                                                Чем строка на питоне:
                                                                PROJECT_HOSTNAME = 'localhost'
                                                                отличается от аналогичной в ini-файле?
                                                                • –2
                                                                  Так и хочется съязвить и сказать, что ковычками…
                                                                  А вообще, наличие секций, например. Ну да, можно dict использовать, или класс… Но это уже сложнее ini, разве нет?
                                                      • +1
                                                        Ой ё! Циклические импорты пошли. Вот попробуйте, кстати, по полученному ImportError понять, что именно в этом дело. Можно и мозг себе будет сломать.
                                                      • 0
                                                        Вот тут, собственно, я искренне пытался решить сферичиский пипец с конфигами в группе довольно крупных джанго-проектов. Вот интересно, как бы вы решили эту проблему?

                                                        P.S. Это не мой код! Я бы до такого не додумался…
                                                        • +8
                                                          1. «все дружно из произвольных мест лезут в волшебный недомодуль settings за своими константами»

                                                          Строго говоря, в django никто не лезет в settings.py проекта за константами. django.conf.settings — это экземпляр класса LazySettings (обертка над классом Settings).

                                                          2. В разделе про исполняемый код было 2 конкретных аргумента.

                                                          Аргумент 1: сложность переопределения вычисляемых значений. Это совсем не проблема, решение тут очевидное: вычисляемые значения вычислять после импорта settings_local.py.

                                                          # значения по умолчанию
                                                          from settings_local import *
                                                          # вычисляемые значения
                                                          


                                                          Аргумент 2:
                                                          Я уже не говорю о том, что исполняемый код в качестве конфигурации может просто приводить к трудноотлаживаемым ImportError при старте приложения в новой среде.


                                                          Можно пример? Если в settings.py синтаксическая ошибка, то будет нормальный трейс с номером строки. Если указана неверная настройка, то будет одинаковая ошибка что в случае ini файла, что в случае settings.py, какая разница, где настройке лежать.

                                                          3. High-coupling. Во-первых, назначение сущности «проект» как раз в том, чтобы связать различные части (которые сами по себе слабо связанные, в идеале) между собой.

                                                          Замечание про VK_API_KEY — справедливое, но оно совершенно не имеет отношения к тому, куда будет помещена опция: в ini-файл или в settings.py. Ваше решение эту проблему не решает никак. Эту проблему решает только здравый смысл — программистам стоит думать, когда упрощение конфигурации за счет выноса константы в глобальные настройки стоит снижения гибкости, а когда нет.

                                                          4. Хранение настроек в ini-файлах (или как угодно) можно легко реализовать в settings.py: создаем там ConfigObj, парсим, что нужно. Этому ну совсем ничего не мешает. А вот чтобы хранить произвольные настройки в .ini, .yaml или каком-то другом формате, нужно будет выдумывать всяческие костыли. Как хранить set в YAML? Как хранить массив в ini? А если настройка — callable (например, функция для транслитерации к какому-нибудь AutoSlugField)?

                                                          Заметьте, то, что, мол, не должно быть callable-настроек, — не аргумент. Они могут иметь смысл и быть простыми. По крайней мере, проще, чем вложенные словари. Запретим вложенные словари? Как решить, что должно быть, а что нет? В пределе получаем, что настроек не должно быть совсем. А должен быть файл, который разные части создает и конфигурирует как нужно. Угу, и получаем опять settings.py, но раздутый в несколько раз конфигурирующим кодом.

                                                          Хитрую подгрузку, иерархию и тд — тоже несложно реализовать, если она вдруг нужна (кстати, зачем?).

                                                          Суть паттерна «settings.py» в том, что он дает гибкость и не мешает ничему, а не в тех ограничениях, которые в статье ему незаслуженно приписываете.
                                                          • +1
                                                            Классный антипаттерн, используется очень много где — это не болезнь питона… phpMyAdmin, Drupal… over 9000 других программ!
                                                            • 0
                                                              (1) Ну хорошо хоть там есть LazySettings. Когда django-style решение слизывают в других проектах, обычно не заморачиваются над такими мелочами.

                                                              (2) А если в settings_local хочется таки определить итоговое значение и чтобы оно потом не перетёрлось? Можно, конечно if 'BLAH_BLAH' not in locals():, но ugly же. Пример с ImportError чуть выше — циклический импорт.

                                                              (3) У вас все проекты заключаются лишь в связывании готовых частей? Везёт вам.

                                                              А про VK_API_KEY — всё так, дело не совсем в настройках. Но если они оформлены в виде модуля, прям возникает соблазн использовать их напрямую. Когда всё лежит в ini при имплементации VkontakteProfileCache внутренний голос спросит: «А точно я именно тут должен заниматься парсеньем настроек? Ведь класс не о том»

                                                              (4) А зачем одновременно иметь и settings.py и ConfigObj? Для dotted-access style notation? Можно ведь объект-конфигурацию унаследовать от базового и дополнить всеми нужными ништяками. В том числе и callable настройками.
                                                              • 0
                                                                Ой, это на самом деле ответ для kmike
                                                                • +2
                                                                  2) видимо, я все еще не понял конечной проблемы
                                                                  # settings_local.py
                                                                  FOO = 'my value'
                                                                  

                                                                  # settings.py
                                                                  FOO = 'default value'
                                                                  from settings_local import *
                                                                  BAR = some_func(FOO)
                                                                  


                                                                  Выше импорта определяем «контракт» — какие у нас настройки, что из них может переопределяться.
                                                                  Ниже импорта — настройки, которые обычно не должны переопределяться в зависимости от окружения и вычисляемые настройки.

                                                                  Если нужно все же переопределить «непереопределяемые» настройки (опять-таки пример для django):
                                                                  # test_settings.py
                                                                  INSTALLED_APPS.remove('my_app')
                                                                  

                                                                  и потом используем опцию --settings=test_settings.py. Ясно, что какие-то более сложные сценарии могут привести к проблемам, но мне непонятно все еще, как именно эти проблемы решают ini-файлы (и как через них, например, реализовать этот test_settings.py).

                                                                  С циклическими импортами в settings.py не сталкивался, видимо, джанга от этого как-то уберегает своими LazySettings.

                                                                  3) Ну да, только в связывании «разных частей», а не «готовых частей». Это же просто точка зрения. Многие из этих частей готовятся специально для проекта, но они от этого частями быть не перестают.

                                                                  4) Справедливое замечание. Его, конечно, можно трактовать и в другую сторону: зачем питоний код, создающий и расширяющий ConfigObj + набор ini-файлов, если можно не городить огород, обойтись просто кодом и держать настройки в одном месте, записанные одним способом (причем таким же немногословным и понятным, как и ini), а не так: часть — в ConfigObj (теряя реализованную каскадность, например? или реализуя ее еще раз для ConfigObj?), часть — в ini. Но замечание справедливое, ConfigObj более гибок, чем мне сначала показалось почему-то.

                                                                  • 0
                                                                    тьфу, пример с test_settings стоит читать вот так, конечно:
                                                                    # test_settings.py
                                                                    from settings import *
                                                                    INSTALLED_APPS.remove('my_app')
                                                                    
                                                                    • 0
                                                                      (2) Проблема не частая, но встречающаяся: иногда вам хочется определить BAR в settings_local.py и не хочется, чтобы затем это было затёрто. В примере из статьи, вероятно, я захочу определить SOME_JOB_COMMAND локально как '/usr/local/bin/do_job -H %s' % PROJECT_HOSTNAME и не захочу, чтобы затем её значение вернули «обратно».
                                                                      • 0
                                                                        У меня возникла мысль… Буквально на правах шутки…
                                                                        А что если пробежаться по settlings.__dict__.values(), выбрать от туда все значения, типы которых строки и сделать им .format(**settlings.__dict__). Ну и соответственно эти строки должны правильно оформляться: '/usr/local/bin/do_job -H {PROJECT_HOSTNAME}'.
                                                                        Тут, правда, возникнет куча сторонних эффектов из-за неизвестного порядка вычисления (что если PROJECT_HOSTNAME тоже вычисляется и его вычисление произойдёт после SOME_JOB_COMMAND?), да и вообще, работать будет только со строками… Но эту идею можно реализовать, и даже красиво. Вот только надо ли? :-)
                                                                        • –1
                                                                          Для параметров-нестрок, можно ещё и eval приплести… Бред, но прикольно. :-)
                                                                          • 0
                                                                            Можно несколько раз пробежать, чтобы разрулить тему с forward-references. Насколько я знаю, так же сделана компиляция в LaTeX.

                                                                            Но да, вопрос: надо ли?! :)
                                                                    • 0
                                                                      А существует хороший путь интеграции этой бадяги в джанго?
                                                                      • 0
                                                                        Тут самое главное — не сделать из django очередной java-фрейморк с xml-файлами для настроек. Накосячить можно и там и там. Это уже проходили.

                                                                        Самое главное — если есть у конкретного продукта некая эко-система — то не стоит вот так сразу пытаться её сломать. В вашем случае получается 2 файла настроек: settings.py и ещё ini-файл. Путаница возникает. Что? Где? Почему именно там?
                                                                        • +1
                                                                          Да что вы упёрлись в свою джангу? В джанге есть подход, который нарушать не стоит.
                                                                          Статья о конфигах вообще. Например, если ты пишешь свой фреймвок или используешь что-нить менее ограниченное, вроде Flask.
                                                                        • +1
                                                                          Было: «Будьте добры иметь в корне settings.py с определёнными FOO_BAR, FOO_BAZ и FOO_QWE», стало «Будьте добры иметь парсер и передавать результат парсинга некого конфига». Чо-та не сильно связанность уменьшилась, не?
                                                                          • +1
                                                                            в нормальных проектах пишут как getattr(settings, 'FOO_BAR', default_value), так-что проблема «обязательно иметь» скорее надумана.
                                                                            • 0
                                                                              А вам не кажется, что питон в данном случае требует скорее словаря?.. settings.get('FOO', 'default') является куда более питоньим подходом в данном случае.
                                                                              • 0
                                                                                ну и получиться ужас вида from settings import settings_dict
                                                                                • 0
                                                                                  Мало понимаю в питоне, когда читал комментарии, пришло в голову как раз использовать что угодно, хоть settings.py, но переменные держать в словаре, как вы написали, и в итоге в начале программы импортировать переменные из словаря по ключу 'default' или 'unittest' в зависимости от надобности.
                                                                                  Чем такой подход плох?
                                                                                  • 0
                                                                                    Т.е. вы предлагаете загрузить сразу все конфиги, а потом, как главный, установить один. Не вижу причин, почему бы не быть такому подходу.

                                                                                    Только тут спор совсем о другом… :-)
                                                                                    • 0
                                                                                      Подумал, подумал, и понял, что да — в исполняемом файле хранить нехорошо. Я тут придумал одно решение, но то же самое может делать и простенький фреймворк, что вы и предлагаете.
                                                                              • 0
                                                                                А это тут причём? При таком подходе главный ужас будет в конфиге, а не при импорте.
                                                                                Я говорю о том, что объект settings должен где-то из чего-то создаваться и, в идеале, быть словарём. И при этом я не говорю где и из чего.
                                                                              • 0
                                                                                По поводу вычисляемых параметров можно сделать так: взять класс (например config), и при обращении к переменным проверять не являются ли они функцией, если же функция — то вычислять и отдавать уже сам результат, т.к. как-то так:

                                                                                Config.PROJECT_HOSTNAME = 'localhost'
                                                                                Config.SOME_JOB_COMMAND = lamda cfg: '%s/bin/do_job.py -H %s' % (cfg.BASE_PATH, cfg.PROJECT_HOSTNAME)
                                                                                Config.PROJECT_HOSTNAME = 'somehost'
                                                                                • 0
                                                                                  Я пытался сделать нечто подобное, но ничего удобного у меня не получилось.
                                                                                  • 0
                                                                                    Как вариант, базовые настройки в BaseConfig(Config), для конкретной стадии в PorductionConfig(BaseConfig), а все вычисляемые методы реализовать через функции. должно получится относительно красиво. Только вот, я не уверен что конфигурационные файлы должны нести в себе логику.
                                                                                    • 0
                                                                                      > Только вот, я не уверен что конфигурационные файлы должны нести в себе логику.
                                                                                      Вот! Золотые слова! :-)
                                                                                      Но иногда её очень хотят…
                                                                                • +3
                                                                                  Какое бурное обсуждение!
                                                                                  А я всегда считал, что бесполезно мешать программисту выстрелить себе в ногу — если он этого так жаждет.
                                                                                  Неблагодарное это дело. Разработчики — люди изобретательные. Они все равно сумеют обойти все ограничения и с наслаждением разрядят ружжо в свою конечность.
                                                                                  • +1
                                                                                    Архитектурные космонавты детектед.

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