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

    Нещодавно я писав пост про те, як був придуманий і написаний 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 і так далі. Така оптимізація може істотно зменшити кількість символів у декларації функції і підвищити читабельність коду.
 
 
Ми працюємо над цим
 
Велика частина коду була написана по-п'яні. Деяка — в дуже сонному стані. Код потребує оптимізації, але вже абияк працює. Розробка і обкатка триває, так що якщо ви раптом вирішите допомогти — приєднуйтесь. Піде будь-яка допомога, а особливо — тестинг. Я, як у старому анекдоті про чукчу, не читач, а письменник. Поки що.
 
Можете писати будь-які пропозиції, побажання та критику тут, у коментарях, або тут .
 
До речі, на превеликий мій жаль травня інгліш з нот соу гуд ес ай нід ту транслейт зис текст, так що якщо хто-небудь цим займеться — буду дуже вдячний :)
 
Друга частина буде злегка езотерічним, але зате, як я сподіваюся, вельми цікавою. Але, на жаль, буде вона не дуже скоро.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.