Дослiдження IT ринку 2019 Go to site!
×Закрыть

Python: Веб-разработка без фреймворков (часть 2)

В прошлой части я постарался рассказать о том, что чистый WSGI код писать не так уж сложно и что преимущества такого подхода налицо, но есть ли у этого обратная сторона? Единственным, пожалуй, недостатком я могу назвать некоторые неудобства по работе с данными в запросе. Был ли запрос GET или POST? Какая у запроса кодировка? Неужели значения формы надо разбирать при помощи cgi.FieldStorage? К счастью эти и многие другие задачи здорово помогает решить WebOb.

webob.Request

В библиотеке WebOb есть класс Request, позволяющий работать с данными из environ с куда большим комфортом. Строки с различными данными из HTTP запроса и от обработавшего его сервера превращаются в удобные в использовании, богатые на функциональность объекты. Доступ к cookies (req.cookies) теперь как к словарю объектов из стандартного модуля Cookie. Переменные формы можно посмотреть в виде как обычных (req.str_GET), так и юникод строк (req.GET). Если хотите, различайте между данными из POST (.POST) и данными из строки запроса (.GET), не хотите — используйте ‘.params’. Charset и MIME-тип из ‘Content-Type’ теперь будут разделены по своим атрибутам (charset, content_type). О scheme, method, remote_addr, referer, user_agent, remote_user итп и говорить нечего.

Очень удобный доступ к пути, по которому было найдено данное WSGI приложение (.script_name, .path, .path_qs, .application_url), какой «хвост» адреса нам предстоит обработать (.path_info, .path_info_peek, .path_info_pop). Есть средства работы с различными полезными адресами, которые пригодятся при генерации ссылок в ответе (.host_url, .application_url, .path_url, .relative_url). Кроме всего прочего это позволяет избежать ситуации, общей для PHP приложений, когда при установке они первым делом спрашивают «ГДЕ Я?»

Если вы хотите поддерживать HTTP стандарт по максимуму, то вам повезло. Атрибут if_modified_since — экземпляр datetime, а обработка значения из заголовка Accept позволяет без хлопот выбрать предпочтительный для клиента формат ответа (.accept.best_match([‘text/html’, ...])).

Пример

Приведу небольшой пример использования этого класса. Представим, что перед нами стоит задача написать форум и мы хотим, чтобы у приложения были красивые URLы и не менее красивое разделение функциональности внутри. Нередко для решения такой задачи пытаются применить какие-то средства фреймворка, но, обычно, оказывается, что инструмент подходит «чуть более чем наполовину», но по большому счету не годится и надо делать всё равно самому (CherryPy). Бывает, пользуются какой-нибудь специальной библиотекой (как например Routes). Нередко умудряются связаться с регулярными выражениями и файлами конфигурации лежащими в специальной папке. А ведь ничего сложного нет:

from webob import Request
def forum_app(environ, start_response):
    req = Request(environ)
    peek = req.path_info_peek()
    if not peek:
        return list_topics_app(environ, start_response)
    elif peek == 'new_topic':
        return new_topic_app(environ, start_response)
    elif peek.isdigit():
        topic_id = int(req.path_info_pop())
        environ['forum_app.topic_id'] = topic_id
        return view_topic_app(environ, start_response)
    else:
        start_response('404 Not Found', [])
        return '404'

‘peek’, как вы возможно и предположили, это фрагмент пути находящийся после адреса, по которому доступен сам forum_app, и до первого следующего слеша («/»). Например, если сам форум расположен по адресу <a href="http://website.com/forum" target="_blank">website.com/forum</a>, то при обработке запроса <a href="http://website.com/forum/" target="_blank">website.com/forum/</a><strong>111</strong>/page-2 таким фрагментом будет ‘111’.

Если такого фрагмента нет, то мы выведем список имеющихся на форуме тем (переложив эту задачу на соответствующее приложение), если этот фрагмент ‘new_topic’, то мы вызываем по цепочке приложение которое займется открытием новой темы. Если же этот фрагмент состоит из цифр, то мы посчитаем что это номер темы, которую надо отобразить. И, если ничто другое не помогло, то выдадим ошибку № 404.

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

Другой немаловажный момент это использование path_info_pop(), этот метод поправит SCRIPT_NAME и PATH_INFO в запросе таким образом, что вызванное нами в дальнейшем приложение (view_topic_app) сможет корректно оценить по какому адресу оно доступно. В частности, вызывая path_info_peek() оно получит уже следующий фрагмент пути, который, в нашем случае, может обозначать, например номер страницы в теме.

Отсебятина

Читатель может справедливо заметить, что код получился не самым чистым из возможных. Это верно, но можно привести всё в порядок при помощи небольшого декоратора. Декоратор впоследствии будет использоваться повсеместно, я бы даже ратовал за включение его в дистрибутив WebOb, но там уже есть нечто похожее, но как по мне куда более неприглядное.

Как мы видим, в forum_app функция start_response непосредственно не вызывается, она только передается далее по цепочке. По большому счету результаты за forum_app генерируют другие приложения (кроме 404, но и на это управа найдется), поэтому давайте согласимся, что принимать нашему приложению положено будет объект Request, а возвращать WSGI приложение. Подчеркиваю, не вызывать а именно возвращать, вызов выполнит уже декоратор. Это можно реализовать вот так:

def webob_wrap(func):
    def wrapped(environ, start_response):
        req = Request(environ)
        app = func(req)
        return app(environ, start_response)
    return wrapped

Код приложения теперь стал хорошо причесанным:

from webob.exc import HTTPNotFound

@webob_wrap
def forum_app(req):
    peek = req.path_info_peek()
    if not peek:
        return list_topics_app
    elif peek == 'new_topic':
        return new_topic_app
    elif peek.isdigit():
        topic_id = int(req.path_info_pop())
        environ['forum_app.topic_id'] = topic_id
        return view_topic_app
    else:
        return HTTPNotFound()

Обратите внимание на HTTPNotFound, такие же приложения есть для всех возможных HTTP ответов: HTTPMovedPermanently(req.host_url + ‘new_path’) итп. Также теперь удобно поменять способ передачи идентификатора темы отображающему её приложению. Для этого следует, вместо использования функции, реализовать view_topic_app как класс, тогда соответствующие строки превратятся в следующее:

    elif peek.isdigit():
        topic_id = int(req.path_info_pop())
        return ViewTopicApp(topic_id)

Возвращать 404 приходится довольно часто, так что стоит поменять в декораторе пару строк:

        app = func(req)
        if app is None:
            app = HTTPNotFound()

Благодаря этому оборачиваемая функция может сообщить что ничего не было найдено просто вернув None. Так что теперь последние строки forum_app можно отбросить.

webob.exc.HTTPException

Модуль webob.exc содержит реализацию HTTP ответов со всевозможными статус-кодами. Удобной особенностью является то, что все эти реализации наследуют от класса HTTPException и при наличии в стеке HTTPExceptionMiddleware из того же модуля, можно делать raise HTTPBadRequest() итп. Повторю, что это одновременно и исключения и WSGI приложения. Это удобно, т.к. некоторые ошибки обработки запроса могут возникать в функциях, которые не имеют непосредственного влияния на ответ. Однако благодаря HTTPException мы можем избежать проверки результатов их работы, полагаясь на то, что в случае ошибки они выбросят соответствующее исключение, которое будет уловлено уровнем выше, в middleware.

(Кстати, этот модуль практически точная копия модуля paste.httpexceptions, но с взаимной совместимостью).

На мой же вкус проще добавить поддержку таких исключений прямо в наш декоратор, благо это увеличит его лишь на три строки, судите сами:

        try:
            app = func(req)
        except HTTPException, app:
            pass

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

from webob import Request
from webob.exc import HTTPException

def webob_wrap(func):
    def wrapped(environ, start_response):
        req = Request(environ)
        try:
            app = func(req)
        except HTTPException, app:
            pass
        if app is None:
            app = HTTPNotFound()
        return app(environ, start_response)
    return wrapped

####

@webob_wrap
def forum_app(req):
    peek = req.path_info_peek()
    if not peek:
        return list_topics_app
    elif peek == 'new_topic':
        return new_topic_app
    elif peek.isdigit():
        topic_id = int(req.path_info_pop())
        return ViewTopicApp(topic_id)

####

from webob import Response
new_topic_app = Response('NEW')
list_topics_app = Response('<br>'.join('<a href=%d>Topic #%d</a>' % (i,i)
                                        for i in range(10)))
def ViewTopicApp(id):
    return Response('VIEW %d' % id)

from paste.httpserver import serve
serve(forum_app)

Про webob.Response речь пойдет в следующей части. Также всё еще на очереди обещанные middleware, mod_wsgi и конфигурация WSGI приложений. Так или иначе упомянуты будут шаблоны и ORM, но до этого еще далеко.

LinkedIn

12 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Сори, недосмотрел 3ю часть цикла))

В описании декоратора: корректнее было бы написать

req = Request(environ, charset='UTF-8').

.

Не уверен что понял вопрос (если он ко мне). Написал статью? Всю серию? Какой-то из комментариев?

А это Вы написали на основе Вашего личного опыта?

Вот оно какое есть — Это жесть, эта — шесть:) Прикольно, сделано оказывается... wsgize интересный. Спасибо, посмотрим, может еще и route пригодится!

«Вишня» так и делает, преобразовывает параметры запроса в аргументы при вызове. Имена атрибутов / методов там сопоставляются с сегментами пути. exposed там помечает публичные методы. Доступ к остальным данным запроса через глобальную переменную (или thread-local).Вот например (почему-то elif не использован где положено): http://www.cherrypy.org/browse...

Угу, пересмотрел. На счет exposed — не знаю, CherryPy не трогал, может и какая совместимость... А суть эксперимента — просто превращение не-WSGI-функции в WSGI app, при этом сама функция сама не знает, что ее ждет. Забавно:) Может и в самом деле эксперимент не очень:) P.S.: Враппилка на WebOb понравилась:)

http://pythonpaste.org/paste/e...Посмотрел, вообще это вроде как или подражание или прослойка совместимости с CherryPy (wsgiapp_wrapper.exposed = True?), налицо недостаточная мощность решения, а кода если и сэкономлено то минимум. На мой взгляд это неудавшийся эксперимент. Если очень хочется чтобы было как в CherryPy, то можно его и пользовать или пропробуйте RhubarbTart, но с опытом желание срезать эти углы проходит.Насчет того что методы оборачивать нужно немного иначе это верно, поэтому я использую другой вариант декоратора (отличий там немало), вот такой: http://pastebin.com/f77982d44

Спасибо, навело на интересные размышления:) Нашел по этому поводу немного — похожий и довольно интересный декораторчик — wsgiapp (paste/evalexception/middleware.py).Вешать можно на функцию и метод. Он так же парсит данные формы, если такая есть.

Подписаться на комментарии