- PVSM.RU - https://www.pvsm.ru -
Сразу хочу объявить, что здесь под аннотациями подразумеваются НЕ декораторы. И я не знаю по какой причине декораторы иногда именуют аннотациями.
Недавно я открыл для себя что в питоне есть фишка, которую я очень давно искал — аннотации к функциям [1]. Это — возможность пихнуть в декларацию функции какую-либо информацию по каждому отдельному её параметру.
Вот каноничный пример из PEP:
def compile(source: "something compilable",
filename: "where the compilable thing comes from",
mode: "is this a single statement or a suite?"):
...
Там же, чуть ниже, приводятся примеры, которые дают понять, что комментирование параметров — не единственное возможное использование данной фичи. Это натолкнуло меня на мысль об одной старой беде, которая досаждала моей нервной системе уже приличное время. А именно — получение данных из форм во Flask [2].
Когда ты хочешь получить аргументы из запроса, ты вынужден брать их сам. Каждый раз. Каждый аргумент. Более того, ты вынужден обрабатывать эти аргументы прямо в теле своего Endpoint. Выглядит это, как правило, не очень дружелюбно.
Вот пример:
@app.route('/ugly_calc')
def ugly_calc():
x, y = int(request.args['x']), int(request.args['y'])
op = OPERATION[request.args['op']]
# Только тут начинается реальное тело функции. Всё, что выше — получение аргументов и их обработка (кстати, очень хреновая обработка)
return str(op(x, y))
Было бы намного логичней получать в контроллер уже очищенные, валидированные и проверенные аргументы:
@app.route('/calc')
def ugly_calc(x:Arg(int), y:Arg(int), op:Arg(_from=OPERATION)):
return str(op(x, y))
Код приобрёл в читаемости и логичности, и размер контроллера уменьшился до фактического количества операций.
Первым делом, нам нужно накидать класс аргумента.
Основу для него мы возьмём вот отсюда [3]. Выкинем то, что нам сейчас не нужно, и вуаля!
class Arg(object):
"""
A request argument.
"""
def __init__(self, p_type=str, default=None):
self.type = p_type
self.default = default
def _validate(self, value):
"""Perform conversion and validation on ``value``."""
return self.type(value)
def validated(self, value):
"""
Convert and validate the given value according to the ``p_type``
Sets default if value is None
"""
if value is None:
return self.default or self.type()
return self._validate(value)
Да, класс аргумента у нас пока что будет очень минималистичным. В конце-концов, расширить его всякими required и передаваемыми валидаторами мы сможем его в любой момент.
Теперь нам нужно сделать штуку, которая будет получать словарь из «грязных» аргументов, и возвращать «чистые».
Тут будет полезно узнать, что аннотации, приписанные функции, формируют словарь, который ложится в атрибут __annotations__.
>>> def lol(yep, foo: "woof", bar: 32*2):
pass
>>> lol.__annotations__
{'foo': 'woof', 'bar': 64}
Итак, как мы видим, у нас есть словарь со всеми элементами, которые нужно обрабатывать. Но про существование других аргументов тоже забывать не следует. Будет не очень хорошо, если функция lol не получит свой yep.
Что-то я отступил от повествования. Продолжаем:
class Parser(object):
def __call__(self, dct):
"""
Just for simplify
"""
return self.validated(dct)
def __init__(self, structure):
self.structure = structure
def validated(self, dct):
for key, arg_instatce in self.structure.items():
dct[key] = arg_instatce(dct.get(key, None))
return dct
Этот класс простой как три рубля. Его инстансы валидируют каждую полученный параметр, название которого есть и в полученном словаре, и в структуре параметров, а потом возвращают изменённый словарь. В общем-то, особого смысла его возвращать и нет, это просто привычка :)
Мы достаточно активно используем дополнительный параметр __annotations__ и декораторы. Поэтому будет лучше дополнить стандартный wraps дабы избежать проблем.
from functools import wraps as orig_wraps, WRAPPER_ASSIGNMENTS
WRAPPER_ASSIGNMENTS += ('__annotations__',)
wraps = lambda x: orig_wraps(x, WRAPPER_ASSIGNMENTS)
Теперь нам нужен простой декоратор для оборачивания целевых функций. Сделаем его в виде класса. Так будет проще.
class Endpoint(object):
"""
Класс для оборачивания целевых функций и передачи им
уже обработанных аргументов вместо сырых
>>> plus = Endpoint(plus)
>>> plus(5.0, "4")
9
"""
def __call__(self, *args, **kwargs):
return self.callable(*args, **kwargs)
def __init__(self, func):
self.__annotations__ = func.__annotations__
self.__name__ = func.__name__
self.set_func(func)
def set_func(self, func):
if func.__annotations__:
# Создаём парсер для данной структуры данных
self.parser = Parser(func.__annotations__)
# Делаем инстансы данного класса вызываемыми.
# Целевая функция оборачивается в декоратор, который
# описн ниже
self.callable = self._wrap_callable(func)
else:
self.callable = func
def _wrap_callable(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
# Обертка принимает все аргументы, предназначенные
# для целефой функции, и передаёт в парсер именованные.
# Только именованные - потому что сюда все данные из
# форм будут приходить именованными
return func(*args, **self.parser(kwargs))
return wrapper
Ну что же, всё готово. Пришло время прикручивать эту штуку к Flask.
Кстати, всё, что мы пилили до этого момента, написано достаточно абстрактно для того, чтобы использовать эти же фрагменты кода и на других фреймворках. И даже без фреймворков :)
Начнём:
class Flask(OrigFlask):
# Для чистого роутинга. Вдруг кому пригодится
froute = OrigFlask.route
def route(self, rule, **options):
"""
Роутим прям как во фласке.
"""
def registrator(func):
# У нас будет правило: 1 метод - 1 эндпоинт.
if 'methods' in options:
method = options['methods'][0]
else:
method = 'GET'
wrapped = self.register_endpoint(rule, func, options.get('name'), method)
return wrapped
return registrator
def register_endpoint(self, rule, func, endpoint_name=None, method='GET'):
endpoint_name = endpoint_name or func.__name__
endpoint = Endpoint(func)
wrapped = self._arg_taker(endpoint)
self.add_url_rule(rule,
"%s.%s" % (endpoint_name, method),
wrapped, methods=[method])
return wrapped
def _arg_taker(self, func):
"""
Эта функция будет забирать аргументы из формы. Такие дела.
"""
@wraps(func)
def wrapper(*args, **kwargs):
for key_name in func.__annotations__.keys():
kwargs[key_name] = request.args.get(key_name)
return func(*args, **kwargs)
return wrapper
Отлично, базовый функционал работает. Пока что без get_args, который я хотел прикрутить, но, думаю, сейчас можно обойтись и без него.
Репа [4]
Можете задавать свои вопросы и предлагать разнообразные фичи, которые можно будет прикрутить.
Автор: uthunderbird
Источник [5]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/59970
Ссылки в тексте:
[1] аннотации к функциям: http://legacy.python.org/dev/peps/pep-3107/
[2] получение данных из форм во Flask: http://flask.pocoo.org/docs/quickstart/#the-request-object
[3] вот отсюда: https://github.com/sloria/webargs/blob/dev/webargs/core.py
[4] Репа: https://bitbucket.org/dsupiev/thunderargs
[5] Источник: http://habrahabr.ru/post/223041/
Нажмите здесь для печати.