воскресенье, 28 июля 2013 г.

Декораторы представлений во Flask

Перевод статьи: View Decorators

В Python имеется любопытная функциональность, которая называется декораторами функций. Декораторы позволяют сделать веб-приложения изящнее. Поскольку каждое представление во Flask является функцией, можно использовать декораторы для добавления дополнительной функциональности к одной или более функций. Декоратор route() - один из тех, который вы возможно уже используете. Но можно реализовать собственные декораторы. Например, представьте, что есть представление, которое должно быть доступно только аутентифицированным пользователям. Если пользователь вошёл на сайт, но не аутентифицировался, его нужно перенаправить на страницу аутентификации. Это хороший случай, в котором декоратор может прийтись как нельзя кстати.

Декоратор "необходима аутентификация"

Ну что ж, давайте реализуем этот декоратор. Декоратор - это функция, возвращающая функцию. На самом деле очень просто. Единственное, о чём нужно помнить при реализации чего-то подобного, это об обновлении __name__, __module__ и других атрибутов функции. Часто об этом забывают, нам не потребуется делать это вручную, потому что есть функция, которая может сделать это за нас и используется в качестве декоратора (functools.wraps()).

Пример подразумевает, что страница для ввода учётных данных называется 'login', а текущий пользователь хранится в g.user. Если пользователь не аутентифицирован, то в g.user хранится None:
from functools import wraps
from flask import g, request, redirect, url_for

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if g.user is None:
            return redirect(url_for('login', next=request.url))
        return f(*args, **kwargs)
    return decorated_function
Итак, как же теперь воспользоваться этим декоратором? Применим его в качестве наиболее глубоко вложенного декоратора. При дальнейшем применении декораторов помните, что декоратор route() должен быть самым внешним:
@app.route('/secret_page')
@login_required
def secret_page():
    pass
Кэширующий декоратор

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

Вот пример функции кэширования. Она использует в качестве ключа для кэша некий префикс (на самом деле это строка формата) и текущий путь запроса. Отметим, что мы создадим функцию, которая создаст декоратор, с помощью которого мы задекорируем функцию. Звучит пугающе? На самом деле это впрямь немного сложнее, но код остаётся достаточно прямолинейным для того, чтобы его ещё можно было прочитать.

Задекорированная функция будет работать следующим образом:
  1. приготовит уникальный ключ кэша для текущего запроса, основываясь на текущем пути.
  2. получит значение для этого ключа из кэша. Если кэш вернул что-нибудь, мы вернём это значение.
  3. в противном случае будет вызвана оригинальная функция, возвращённое значение будет помещено в кэш с указанным временем кэширования (по умолчанию - 5 минут).
Вот код:
from functools import wraps
from flask import request

def cached(timeout=5 * 60, key='view/%s'):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            cache_key = key % request.path
            rv = cache.get(cache_key)
            if rv is not None:
                return rv
            rv = f(*args, **kwargs)
            cache.set(cache_key, rv, timeout=timeout)
            return rv
        return decorated_function
    return decorator
В этом примере подразумевается, что объект cache уже инициализирован, как описано в разделе Кэширование.

Шаблонизирующий декоратор

В TurboGears некоторое время назад было распространено использование шаблонизирующих декораторов. Смысл этого декоратора заключается в том, что функция представления возвращает словарь со значениями для шаблона, а шаблон отрисовывается автоматически. В этом случае следующие три примера делают одно и то же:
@app.route('/')
def index():
    return render_template('index.html', value=42)

@app.route('/')
@templated('index.html')
def index():
    return dict(value=42)

@app.route('/')
@templated()
def index():
    return dict(value=42)
Как можно заметить, если имя шаблона не указано, используется конечная точка карты URL с точками, преобразованными в косые черты и с добавленным справа текстом '.html'. В противном случае, используется шаблон с указанным именем. Когда задекорированная функция завершается, возвращённый ею словарь передаётся в функцию отрисовки шаблона. Если ничего не возвращено, подразумевается пустой словарь, а если возвращён не словарь, мы возвращаем это значение неизменным. Таким образом по-прежнему можно пользоваться функцией redirect или возвращать обычные строки.

Вот код этого декоратора:
from functools import wraps
from flask import request

def templated(template=None):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            template_name = template
            if template_name is None:
                template_name = request.endpoint.replace('.', '/') + '.html'
            ctx = f(*args, **kwargs)
            if ctx is None:
                ctx = {}
            elif not isinstance(ctx, dict):
                return ctx
            return render_template(template_name, **ctx)
        return decorated_function
    return decorator
Декоратор конечной точки

Если хочется воспользоваться системой маршрутизации werkzeug для достижения большей гибкости, нужно отобразить конечную точку в соответствии с правилом для для функции представления. Это можно сделать с помощью декоратора. Например:
from flask import Flask
from werkzeug.routing import Rule

app = Flask(__name__)
app.url_map.add(Rule('/', endpoint='index'))

@app.endpoint('index')
def my_index():
    return "Hello world"
Примечания переводчика

Этот и другие переводы можно найти на сайте проекта перевода документации по Flask. Автор проекта - Виталий Кузьмин aka ferm32.

Комментариев нет: