📜 ⬆️ ⬇️

One use of annotations

Just want to declare that here the annotations are NOT meant as decorators. And I do not know for what reason decorators are sometimes called annotations.

Recently, I discovered that there is a chip in python, which I have been looking for a long time - annotations to functions . It is an opportunity to shove any information on a function's every parameter in the function declaration.

Here is a canonical example from PEP:
')
def compile(source: "something compilable", filename: "where the compilable thing comes from", mode: "is this a single statement or a suite?"): ... 


In the same place, just below, there are examples that make it clear that commenting parameters is not the only possible use of this feature. This gave me the idea of ​​an old disaster that plagued my nervous system for a decent time. Namely - getting data from forms in Flask .



Problem



When you want to get arguments from a query, you have to take them yourself. Everytime. Every argument. Moreover, you are forced to process these arguments directly in the body of your Endpoint. It looks, as a rule, not very friendly.

Here is an example:

 @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)) 


It would be much more logical to get to the controller already cleared, validated and verified arguments:

 @app.route('/calc') def calc(x:Arg(int), y:Arg(int), op:Arg(_from=OPERATION)): return str(op(x, y)) 


The code acquired in readability and consistency, and the size of the controller decreased to the actual number of operations.

Well, we drove



First of all, we need to distribute the class of the argument.

We will take the basis for it from here . Let's throw out what we do not need now, and voila!

 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) 


Yes, the class of argument we have so far will be very minimal. In the end, we can expand it with all sorts of required and transmitted validators at any time.

Now we need to do the thing that will receive the dictionary from the “dirty” arguments, and return the “clean” ones.

Here it will be useful to know that the annotations, the assigned functions, form the dictionary, which falls in the attribute __annotations__ .

 >>> def lol(yep, foo: "woof", bar: 32*2): pass >>> lol.__annotations__ {'foo': 'woof', 'bar': 64} 


So, as we see, we have a dictionary with all the elements that need to be processed. But about the existence of other arguments, too, should not be forgotten. It will not be very good if the lol function does not get its yep .

Something I retreated from the story. We continue:

 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 


This class is as simple as three rubles. His instances validate each parameter received, the name of which is in the resulting dictionary, and in the structure of parameters, and then return the modified dictionary. In general, there’s no special reason to return it, no, it's just a habit :)

We rather actively use the additional parameter __annotations__ and decorators. Therefore, it would be better to add a standard wraps order to avoid problems.

 from functools import wraps as orig_wraps, WRAPPER_ASSIGNMENTS WRAPPER_ASSIGNMENTS += ('__annotations__',) wraps = lambda x: orig_wraps(x, WRAPPER_ASSIGNMENTS) 


Now we need a simple decorator to wrap the objective functions. Let's make it in the form of a class. It will be easier.

 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 


Well, everything is ready. It's time to screw this thing to Flask.
By the way, everything we sawed up to this point is written in a rather abstract way in order to use the same code fragments in other frameworks. And even without frameworks :)

Let's start:
 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 


Great, basic functionality works. So far, without _from, but I think you can do without it now.

Turnip

You can ask your questions and offer a variety of features that can be screwed.

UPD


I wrote a short manual on the use of this thing.

Source: https://habr.com/ru/post/223041/


All Articles