Категории

Язык
Окружение
Область
Интерпретатор
Проект
Аспект языка

29 сентября 2014 г. 18:54 (ред. 23 марта 2017 г. 13:19)
Небольшой рассказ о том, как и нужно ли искать компромисс между совестью питониста — Дзеном Питона — и удобством ПИП (API).
Однажды, где-то в районе августа 2013 года, мне на ум пришла идея очередного приложения для Django. Идея заключалась в том, чтобы дать пользователям возможность управлять настройками приложения (обычно они расположены в settings.py этого самого приложения) через административный интерфейс Django.

В обязательном порядке, перед тем как создавать что-то новое, требуется проверить, нет ли уже готового решения.

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

Разработка идеи началась с изучения возможности использования в settings.py некой функции-регистратора, которая могла бы указывать на имена [в пространстве имён файла], хранящие настройки. Хотелось, чтобы функция регистрации использовала не строки с именами, а сами имена, а частности, чтобы IDE могли понять, что откуда.

Примерно так:
    A = True
B = True
C = False
D = 42

store_values(A, B, C, D)

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

Как видите, в первом же комментарии мне рассказали о том, что это бессмысленная задача, которую никогда никому решать не придётся. Ну, и вариант решения предложили соответствующий — используй строки с именами и никаких гвоздей. И ещё напугали, что все другие подходы от лукавого (завязаны на чёрную магию и против Дзена), угнетают пользователей, не соответствуют модели данных Питона и такие хрупкие, что вот-вот сломаются. А дальше и вовсе закрыли вопрос с резолюцией: «Вопрос должен демонстрировать хотя бы минимальное понимание решаемой задачи». Пусть так %P

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

И решение нашлось, и оно оформилось в приложение django-siteprefs. Выглядит решение примерно так:

  # Внутри settings.py вашего приложения.

... # Здесь уже объявлены ранее наши псевдоконстанты настроек.

if 'siteprefs' in settings.INSTALLED_APPS: # Уважаем право людей не использовать siteprefs.
from siteprefs.toolbox import patch_locals, register_prefs, pref, pref_group

# Чтобы пользователи не удивлялись, почему некоторые локальные переменные
# содержат странные прокси-объекты, заставляем их патчить переменные вручную.
patch_locals()

register_prefs( # Регистрируем настройки.
# Например, зарегистрируем группу настроек, относящихся к работе с эл. почтой.
# И разрешим их редактирование в адм. интерфейсе (static=False)
pref_group('Почтовые настройки', (ENABLE_MAIL_RECOVERY, ENABLE_MAIL_BOMBS), static=False),
SLOGAN, # Эта настройка останется неизменяемой, просто выведется в адм. интерфейсе.
# Ну, и ещё одна не статичная настройка, с добавлением описаний.
pref(ENABLE_GRAVATAR_SUPPORT,
verbose_name='Использовать Gravatar',
static=False,
help_text='Разрешает использование аватаров с сервиса Gravatar.'),
)

Как это всё работает? Ни дорожной пыли, ни болотной гнили, ни чёрных, ни белых заклинаний. В основе лежит использование инструментов из модуля inspect.
Пользователь, вызывая patch_locals, собственноручно соглашается на внесение изменений в локальный контекст. Какого рода изменения и зачем они нужны?

Чтобы узнать к какому имени какое значение привязано, нам требуется пройти по именам локального контекста и отыскать объект идентичный переданному в функцию-регистратор. Первое, что приходит на ум, использовать id объекта, но тут на нашем пути встаёт сам интерпретатор, кеширующий некоторые значения (булево, мелкие целые и пр.). Так, если вернуться к первому примеру, невозможно определить какая True попала в функцию — из A или из B. Поэтому нам остаётся только заменить все объекты в нужном контексте на наши собственные PatchedLocal, имеющие заведомо разные идентификаторы. Добраться до локальных символов в нужном фрейме нам позволяет тот самый inspect.

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

Что получилось в итоге: чёрная магия, которую нам сулили, оказалась не такой уж чёрной; пользователю, смею надеяться, удивляться не приходится; работает приложение, вроде, стабильно. Но главное в нашем деле — ПИП (API), который похоже, не вызывает у людей отторжения (примерно 2 тысячи скачиваний с PyPI в месяц).

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

Иногда полезно идти наперекор.
Удачи вам.


P.S.: Вот, что ещё вспомнил. Слыхал, некоторые интересуются взлетит ли проект pythonz.net, а если взлетит, то когда. Отвечаю: мы тут про пресмыкающихся говорим. Ползём дальше.