Вообще-то, это картинка от wtforms, но у меня гимп почему-то не запускается.
Эту статью я пишу в баре. Очень хочется похоливарить, но бармен на меня смотрит круглыми глазами, а кальянщик просто улыбается и мотает головой.
Однажды, меня спросили: что плохого во flask? Тогда меня полностью устраивал этот милый фреймворк. Поработав с ним какое-то время, я написал все, что думаю, в рабочий слак, на что мне ответили: "Мурад, будь добрее". Вообще, я добрый и пушистый, но wtf?!
Стоит отметить, что я являюсь большим поклонником работы Армина. Его пакеты используются во многих мои проектах. А еще он невероятная заноза в сообществе Python. И это хорошо.
Flask
Flask — это обертка над очень крутыми обособленными проектами. Некоторые сравнивают его с джангой. Я хз почему.
Если попытаться описать все проблемы фласка в двух пунктах:
- импорты
- контекст реквеста
Все. Дальше можно не читать. Но если все еще не понятно, листаем дальше.
Blueprints
Если в джанге все приложения подключаются в INSTALLED_APPS
, то во фласке используется концепция blueprints
. Этакое приложение и обособленный неймспейс урлов в одном флаконе:
from flask import Flask
from yourapplication.simple_page import simple_page
app = Flask(__name__)
app.register_blueprint(simple_page, end_point='/simple_page')
Далее, можно роутить урлы так: url_for('simple_page.index')
.
Вложенность
А ее нет. Нельзя просто так взять и сделать вложенный неймспейс. В сети можно найти "решения", но здесь я буду рассматривать только коробочный фласк, потому что написать-то можно все.
Импорты
Когда вы делаете так в продакшен коде:
if foo == 3:
do_stuff(foo)
Где-то в мире грущу я! Берегите меня, выносите это в сеттинги.
from myapp import app
class Foo(FlaskForm):
choices = SelectField(choices=app.conf.FOO_CHOICES)
Концептуально. Но работать не будет. Потому что пару строчек назад мы импортировали пакет в myapp
и навечно заказали себе путь туда.
Погодите, должен быть выход! Ага!
from flask import current_app
И это не работает. Потому что current_app
доступен только в контектсте реквеста!
— Вынеси это наконец в файл сеттингов приложения, больной ублюдок! — голос из зала.
— А как я буду их подменять на проде или тестовом, а?
Кстати, для джанги на этот случай у меня есть специальная батарейка.
Просто представьте себе прекрасный дивный мир, где django.conf.settings
доступен только в контексте реквеста!
flask.g
Нельзя не пошутить про одноименную точку. А главное, ее не нужно искать, она всегда тут: flask.g
. Бада-бум-тсс!
Вот поэтому Армин — мой кумир!
В нее можно пробросить все необходимое:
@app.before_request
def before_request():
g.locale = get_locale()
g.foo = foo
Однако, это будет работать только в контексте реквеста, как и любые другие магические объекты фласка.
Роутинг урлов и их методы
У меня на сайте есть такой кусок урлов:
bp.add_url_rule('/api/v1/', view_func=ApiView.as_view('api_view'))
...
bp.add_url_rule('/<path:path>/', view_func=PageView.as_view('page_view'))
ApiView
обрабатывает только один метод — POST
. Угадайте, что будет если спросить GET
? Ага, 404. Ее обеспечивает вторая вьюшка.
Чтобы получить NOT ALLOWED
, нужно явно вернуть 405 в ApiView
!
Fask, что с тобой не так?
Стейк!
А. Погодите. Это мне. Омн-омн-омн.
flask-wtf. CSRF
Ох. Допустим, нам нужно отключить проверку в одной вьюхе:
@app.route('/foo', methods=('GET', 'POST'))
@csrf.exempt
def my_handler():
# ...
return 'ok'
Значит, нам нужен app
. Помните про импорты, да? Ищем выход, лезем в сорцы:
def exempt(self, view):
...
if isinstance(view, string_types):
view_location = view
else:
view_location = '.'.join((view.__module__, view.__name__))
self._exempt_views.add(view_location)
return view
Ура! Можно передать путь до вьюхи (в версии, которая вышла две недели назад)! Пробуем:
csrf.exempt('website.apps.typus_web.views.ApiView')
Не работает. На самом деле (ненавижу это выражение), мы подменили имя вьюхи, когда вызывали ApiView.as_view('api_view')
:
csrf.exempt('website.apps.typus_web.views.api_view')
И "все равно", что мы указываем путь до объекта, которого нет. Работает! Не работает.
А знаете почему? Потому что форма. Она ничегошеньки не знает про вьюху:
class ApiForm(ViewForm):
...
class Meta(ViewForm.Meta):
csrf = False
Вот теперь работает.
url_for()
Допустим, вы хотите сделать так:
NAVIGATION = (
(url_for('flatpages:index'), _('Home page')),
)
Забудьте. Вне контекста не работает. Наверное, можно сделать свой ленивый объект, в конце-концов, в джанге это тоже не сразу появилось.
flask-testing
Штука, призванная помочь с тестами. Например, можно заглянуть в контекст, который передается в шаблон. А давайте попробуем:
AssertionError: Popped wrong request context.
Ой. Что-то пошло не так. А знаете что? Я вот тоже не знаю.
На самом деле (ненавижу это выражение), я схватил NotImplementedError
в одном из методов, которые не переопределил. Но поинт в том, что уронив тесты, вам ни за что не понять в чем причина.
Всякое разное
В процессе ковыряния фласка, нашел несколько моментов:
def jsonify(*args, **kwargs):
...
if args and kwargs:
raise TypeError('jsonify() behavior undefined when passed both args and kwargs')
elif len(args) == 1: # single args are passed directly to dumps()
data = args[0]
else:
data = args or kwargs
Здесь что-то происходит. Это все, что я понимаю.
И это:
def make_response(*args):
if not args:
return current_app.response_class()
if len(args) == 1:
args = args[0]
return current_app.make_response(args)
А теперь ягодки:
class Flask(_PackageBoundObject):
def make_response(self, rv):
status_or_headers = headers = None
if isinstance(rv, tuple):
rv, status_or_headers, headers = rv + (None,) * (3 - len(rv))
Все, жгите!
P.S. А чего я один эээ статьи пишу, м? Кто хочет на той неделе, скажем, во вторник (чтобы больше вместить) пойти в бар (в Питере, в районе Звездной)?.. Пишите в инбокс.
P.P.S. Хабр, ты почему не типографишь тексты? Вот, я даже штуку написал!