python

Thunderargs: практика использования. Часть 1

  • воскресенье, 1 июня 2014 г. в 03:10:55
http://habrahabr.ru/post/224705/

Недавно я писал пост про то, как был придуман и написан thunderargs. Сегодня я раccкажу о том, как его можно применять.

Напомню, что эта штука предназначена для обработки параметров функции при помощи аннотаций. Например, так:

OPERATION = {'+': lambda x, y: x+y,
             '-': lambda x, y: x-y,
             '*': lambda x, y: x*y,
             '/': lambda x, y: x/y,
             '^': lambda x, y: pow(x,y)}

@Endpoint
def calculate(x:Arg(int), y:Arg(int),
                       op:Arg(str, default='+', expander=OPERATION)):
    return str(op(x,y))


Постараемся по ходу тутора решать вполне определённые проблемы, а не какие-то эфемерные задачки. Ну а теперь — к делу.


Сегодня во всех примерах (или почти во всех) мы будем использовать Flask. Те, кто хотя бы немного знаком с этим фреймворком, прекрасно знают, что проблема извлечения аргументов из форм — боль и унижение. Ну и кроме того, в прошлом топике я уже написал кусок, который позволяет использовать thunderargs в связке с flask без лишних заморочек.

Кстати, можете забрать весь код, приведённый в примерах, отсюда. Вам нужен файлик flask-example.

Поехали



Шаг 0: синтаксис аннотаций, или избавляемся от эффекта магии


Подробнее про синтаксис аннотаций можно почитать здесь.

А здесь мы рассмотрим только то, что нам действительно нужно: синтаксис описания аргументов. Делается это так:

def foo(a: expression, b: expression):
    ...


После этого мы можем получить доступ к описанию аргументов через поле __annotations__ функции foo:

>>> def foo(a: "bar", b: 5+3):
	pass

>>> foo.__annotations__
{'b': 8, 'a': 'bar'}


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

Шаг 0.5: установка


Я таки залил эту штуковину в PyPI по просьбе какого-то чувака, так что можете смело ставить её через pip. Единственная поправка: некоторые особенности, которых мы коснёмся в мануале, есть только в альфа-версии, так что советую использовать --pre:

sudo pip install thunderargs --pre


И не забудьте поставить flask! По идее, он не обязателен для работы самого thunderargs, но в текущем мануале мы будем его пользовать.

Шаг 1: элементарное приведение типов


Самый простой вариант использования thunderargs — приведение типов. У Flask есть такая малоприятная особенность: он не имеет никаких средств для предварительной обработки аргументов, и их приходится обрабатывать прямо в теле функций-эндпоинтов.

Предположим, что мы хотим написать простую пагинацию. У нас будет два параметра: offset и limit.

Всё, что для этого нужно — указать тип данных, к которому данные аргументы должны быть приведены:

from random import randrange

from thunderargs.flask import Flask

app = Flask()

# Just a random sequence
elements = list(map(lambda x: randrange(1000000), range(100)))

@app.route('/step1')
def step1(offset: Arg(int), limit: Arg(int)):
    return str(elements[offset:offset+limit])

if __name__ == '__main__':
    app.run(debug=True)


Прошу заметить, что здесь и далее я использую не классический Flask, а версию с заменённой функцией route, которую импортирую из thunderargs.flask.

Итак, мы смогли запихнуть приведение типов в аннотации, и теперь нам больше не придётся делать дурацкие операции типа таких:

    offset = int(request.args.get('offset'))
    limit = int(request.args.get('limit'))


в теле функции. Уже неплохо. Но вот беда: есть ещё огромное число не учтённых вероятностей. Что если кто-то догадается ввести отрицательное значение limit? Что если кто-то вообще не укажет никакого значения? Что если кто-то введёт не число? Не беспокойтесь, средства для борьбы с этими исключениями есть, и мы их рассмотрим.

Шаг 2: значение по умолчанию


Пока нам подходит наш текущий пример, не будем придумывать ничего нового, просто будем дополнять его.

Значения по умолчанию, на мой взгляд, задаются весьма интуитивно:

@app.route('/step2')
def step2(offset: Arg(int, default=0),
          limit: Arg(int, default=20)):
    return str(elements[offset:offset+limit])


Останавливаться на этом подробнее пока не будем. Кроме, пожалуй, одного факта: дефолтное значение должно являться инстансом указанного класса. В нашем случае, например, [0,2,5] в качестве дефолтного значения не прокатит.

Шаг 3: обязательный аргумент


@app.route('/step3')
def step3(username: Arg(required=True)):
    return "Hello, {}!".format(username)


Думаю, с кодом всё ясно. Но я должен кое-что прояснить: одновременно использовать и default и required нельзя. Такая попытка будет рейзить ошибку. Это своего рода предохранитель от возможной логической ошибки, которую потом будет очень трудно найти.

А если вы не дадите серверу нужный ему аргумент, то получите ошибку thunderargs.errors.ArgumentRequired.

Шаг 4: множественный аргумент


Тут всё тоже достаточно очевидно. Кроме, возможно, того, что в качестве параметра в нашу функцию придёт map object, не список.

@app.route('/step4')
def step4(username: Arg(required=True, multiple=True)):
    return "Hello, {}!".format(" and ".join(", ".join(username).rsplit(', ', 1)))


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

?username=John&username=Adam&username=Lucas


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

Шаг 5: валидаторы


Вернёмся к нашему примеру с пагинатором, который мы обещали довести до ума.

from thunderargs.validfarm import val_gt, val_lt

@app.route('/step5')
def step5(offset: Arg(int, default=0, validators=[val_gt(-1),
                                                  val_lt(len(elements))]),
          limit: Arg(int, default=20, validators=[val_lt(21)])):
    return str(elements[offset:offset+limit])


Валидаторы создаются на ферме фабрик под названием validfarm. Там сейчас только примитивнейшие варианты, вроде len_gt, val_neq и так далее, но в дальнейшем, думаю, список будет пополняться.

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

def step5(offset: Arg(int, default=0, validators=[lambda x: x >= 0 and
                                                            x < len(elements)]),
          limit: Arg(int, default=20, validators=[val_lt(21)])):
    ...


Или даже так:

def less_than_21(x):
    return x < 21

@app.route('/step5_5')
def step5_5(offset: Arg(int, default=0, validators=[lambda x: x >= 0 and
                                                            x < len(elements)]),
          limit: Arg(int, default=20, validators=[less_than_21])):
    ...


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

Шаг 6: разворачиваем аргументы


Очень часто бывает так, что нам нужно получить от пользователя ключ, который укажет нам с каким аргументом нам предстоит работать. Именно для этого случая нам и нужна развёртка.

Для демонстрации снова приведу функцию, которую уже упомянул в начале. Думаю, на этот раз тут всё будет понятнее.

OPERATION = {'+': lambda x, y: x+y,
             '-': lambda x, y: x-y,
             '*': lambda x, y: x*y,
             '^': lambda x, y: pow(x,y)}

@app.route('/step6')
def step6(x:Arg(int), y:Arg(int),
          op:Arg(str, default='+', expander=OPERATION)):
    return str(op(x,y))


Как вы видите, op у нас вытаскивается по ключу, который мы получили от пользователя.

expander может быть словарём или вызываемым объектом. Туда можно пихнуть функцию, которая, например, вытащит для нас нужный объект из базы по заданному ключу.

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

Внемануальные замечания



Python 2 или вариант для ископаемых


В принципе, я не использовал ничего такого, что сделало бы невозможным обратный перенос данной софтинки во второй питон. Нужно заменить форматирование строк. Да и, пожалуй, всё. Для эмуляции аннотаций в модуле thunderargs.endpoint есть простенький декоратор под названием annotate. Если вкратце, пользоваться им так:

@annotate(username=Arg())
def foo(username):
    ...


В теории должно работать, хотя на практике не тестил.

Мелкие заметки по фласку


В статье мы рассматривали исключительно метод GET, но это не значит, что другие методы не поддерживаются. Я выбрал его просто чтобы не заморачиваться. Но тут есть одна тонкость: я считаю, что множественные методы для одной целевой функции не нужны, и поэтому теперь каждая функция может отвечать только за один метод. На мой взгляд это делает код читабельней.

Если вам вдруг очень понадобится родной фласковый роутинг — используйте app.froute. Но не забывайте, что там фишки с аннотациями не работают.

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

Можно использовать одновременно и path variables, и параметры из аннотаций. Они не конфликтуют между собой, если какой-нибудь из параметров не является одновременно и тем, и другим.

Мелкие заметки по не-фласку


Следует помнить, что thunderargs прекрасно работает и без фласка. Для этого вам нужно использовать на функциях декоратор Endpoint из thunderargs.Endpoint.

Не стоит, однако, злоупотреблять этим. Реально хардкорная обработка аргументов нужна только на контроллерах.

Не забывайте, что вы легче лёгкого можете создать своих потомков от Arg. IntArg, StringArg, BoolArg и так далее. Такая оптимизация может существенно уменьшить количество символов в декларации функции и повысить читабельность кода.

Мы работаем над этим


Большая часть кода была написана по-пьяни. Некоторая — в очень сонном состоянии. Код нуждается в оптимизации, но уже кое-как работает. Разработка и обкатка продолжается, так что если вы вдруг решите помочь — присоединяйтесь. Пойдёт любая помощь, а особенно — тестинг. Я, как в старом анекдоте про чукчу, не читатель, а писатель. Пока что.

Можете писать любые предложения, пожелания и критику здесь, в комментариях, либо здесь.

Кстати, к большому моему сожалению май инглиш из нот соу гуд эс ай нид ту транслэйт зис текст, так что если кто-нибудь этим займётся — буду очень благодарен :)

Вторая часть будет слегка эзотеричной, но зато, как я надеюсь, весьма интересной. Но, к сожалению, будет она не очень скоро.