Recently, I wrote a post about how
thunderargs was coined and written. Today I will talk about how it can be applied.
Let me remind you that this thing is designed to process the parameters of the function using annotations. For example:
OPERATION = {'+': lambda x, y: x+y, '-': lambda x, y: xy, '*': 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))
')
We will try to solve quite definite problems in the course of the splint, and not some ephemeral problems. And now - to the point.
Today in all examples (or almost all) we will use Flask. Those who are at least a little familiar with this framework are well aware that the problem of extracting arguments from forms is pain and humiliation. Well, besides, in the past topic, I have already written a piece that allows you to use thunderargs in conjunction with flask without unnecessary problems.
By the way, you can take all the code given in the examples
from here . You need a flask-example file.
Go
Step 0: syntax annotations, or get rid of the effect of magic
You can read more about the syntax of annotations
here .
And here we will consider only what we really need: the syntax for describing arguments. This is done like this:
def foo(a: expression, b: expression): ...
After this, we can access the description of the arguments through the
__annotations__
field of the
__annotations__
function:
>>> def foo(a: "bar", b: 5+3): pass >>> foo.__annotations__ {'b': 8, 'a': 'bar'}
As we can see, here we have the names of the annotated variables and the calculated expressions as dictionary values. This means that we can push in the annotation any arbitrary expressions that will be calculated during the function declaration. This is the chip we use. If you want to know how exactly - you are welcome to read the post, the link to which is given at the beginning of this.
Step 0.5: Installation
I poured this thing into PyPI at the request of some dude, so you can safely put it through pip. The only amendment: some features that we touch on in the manual are only in the alpha version, so I advise you to use
--pre
:
sudo pip install thunderargs --pre
And do not forget to put the flask! In theory, it is not necessary for the work of the thunderargs itself, but in the current manual we will use it.
Step 1: elementary type conversion
The easiest way to use thunderargs is type casting. Flask has such an unpleasant feature: it has no means for preprocessing arguments, and they have to be processed directly in the body of endpoint functions.
Suppose we want to write a simple pagination. We will have two parameters: offset and limit.
All that is needed for this is to specify the data type to which the given arguments should be given:
from random import randrange from thunderargs.flask import Flask app = Flask()
Please note that hereinafter I’m not using the classic Flask, but the version with the replaced
route
function, which I import from
thunderargs.flask
.
So, we were able to stuff type conversions into annotations, and now we no longer have to do stupid operations like these:
offset = int(request.args.get('offset')) limit = int(request.args.get('limit'))
in the body of the function. Already not bad. But the trouble is: there is still a huge number of uncounted probabilities. What if someone guesses to enter a negative limit value? What if someone doesn’t indicate anything at all? What if someone does not enter a number? Do not worry, there are means to deal with these exceptions, and we will consider them.
Step 2: Default Value
As long as our current example fits, we will not invent anything new, just complement it.
The default values, in my opinion, are very intuitive:
@app.route('/step2') def step2(offset: Arg(int, default=0), limit: Arg(int, default=20)): return str(elements[offset:offset+limit])
We will not dwell on this in more detail. Except, perhaps, one fact: the default value must be an instance of the specified class. In our case, for example,
[0,2,5]
as a default value does not roll.
Step 3: Required Argument
@app.route('/step3') def step3(username: Arg(required=True)): return "Hello, {}!".format(username)
I think everything is clear with the code. But I have to clarify something: you cannot use both default and required at the same time. Such an attempt would raise an error. This is a kind of fuse against a possible logical error, which then will be very difficult to find.
And if you do not give the server the argument it needs, you will get an error
thunderargs.errors.ArgumentRequired
.
Step 4: Multiple Argument
It's all pretty obvious too. Except, possibly, the fact that a
map object
, not a list, comes to our function as a parameter.
@app.route('/step4') def step4(username: Arg(required=True, multiple=True)): return "Hello, {}!".format(" and ".join(", ".join(username).rsplit(', ', 1)))
If someone has already forgotten, such arguments are thrown to us when the user specifies a set of values for one name. Request example:
?username=John&username=Adam&username=Lucas
Of course, the default value in this case must be submitted in the form of a list, and each argument of this list must satisfy all the conditions.
Step 5: Validators
Let us return to our example with the paginator, which we promised to bring to mind.
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])
Validators are created on a factory farm called validfarm. There are now only the most primitive options, like len_gt, val_neq, and so on, but in the future, I think, the list will be updated.
But no one bothers to make us your validator. It must simply be a function that receives a value and returns a boolean response, whether it satisfies this value or not.
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)])): ...
Or even like this:
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])): ...
In general, absolutely any piece that can be called, which can work when only one argument is passed to it, and which returns a Boolean response, will fit as a validator.
Step 6: Expand Arguments
It often happens that we need to get a key from the user who will tell us what argument we have to work with. It is for this case that we need a scan.
To demonstrate again, I will give the function that I already mentioned at the beginning. I think this time everything will be clearer.
OPERATION = {'+': lambda x, y: x+y, '-': lambda x, y: xy, '*': 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))
As you can see,
op
is pulled out of us by the key that we received from the user.
expander
can be a dictionary or called object. There you can shove a function that, for example, pulls out for us the desired object from the database for a given key.
For today we will finish with the review of functionality. The only thing I would like to make a couple of extraordinary remarks.
Out of notice
Python 2 or fossil version
In principle, I did not use anything that would make it impossible to transfer this softphone back to the second python. It is necessary to replace formatting of lines. And, perhaps, everything. To emulate annotations in the module
thunderargs.endpoint
there is a simple decorator called
annotate
. In short, use it like this:
@annotate(username=Arg()) def foo(username): ...
In theory, it should work, although in practice it was not tested.
Small notes on flamen
In the article we considered only the GET method, but this does not mean that other methods are not supported. I chose it just to not bother. But there is one subtlety: I believe that multiple methods for one objective function are not needed, and therefore now each function can only be responsible for one method. In my opinion, this makes the code more readable.
If you suddenly need your own native routing, use
app.froute
. But do not forget that the annotated chips do not work there.
In theory, interfacing with other modules of the flake should not break. But practice will show.
You can use both path variables and parameters from annotations simultaneously. They do not conflict with each other if one of the parameters is not both.
Small non-flag notes
It should be remembered that thunderargs works fine and without flapping. To do this, you need to use the
Endpoint
decorator from
thunderargs.Endpoint
on the functions.
Do not, however, abuse it. Really hardcore processing of arguments is needed only on controllers.
Do not forget that you can easily create your descendants from
Arg
. IntArg, StringArg, BoolArg, and so on. Such optimization can significantly reduce the number of characters in the function declaration and increase the readability of the code.
We are working on it
Most of the code was written drunk. Some - in a very sleepy state. The code needs to be optimized, but it works somehow. Development and running continues, so if you suddenly decide to help - join. Any help will come, especially testing. I, as in the old joke about the Chukchi, not a reader, but a writer. For now.
You can write any suggestions, suggestions and criticisms here, in the comments, or
here .
By the way, much to my regret, May English from notes sou gud es ai nid that translate zis text, so if someone does this, I will be very grateful :)
The second part will be slightly esoteric, but, I hope, very interesting. But, unfortunately, it will not be very soon.