The Python adorner is a powerful tool for eliminating redundancy. With the ability to block functionality into the right size, even the most complex workflow, adorners can make it a neat feature.
For example, let's look at the Django Web framework, which processes the requested method to receive a method object and returns a response object:
def handle_request (Request): return HttpResponse ("Hello, World")
I recently encountered a case where I need to write several API methods that meet the following criteria:
- Return JSON response
- If it is a GET request, then the error code is returned
As an example of registering API endpoints, I will write like this:
def register (Request): result = None # Check for post only if request.method! = ' Post ': result = {"Error": "This method is only accepts posts!"} else: try: user = User.objects.create_user (request. post[' username '), request. post[' email ', request. post[' password ') # Optional Fields for field in [' First_Name ', ' last_name ']: if field in request. POST: setattr (User, field, request.) Post[field]) user.save () result = {"Success": True} except Keyerror as e: result = {"error": Str (e) } response = HttpResponse (Json.dumps (Result)) if "error" in result: Response.status_code = return response
However, I will write the JSON response and the code returned by the error in each API method. This will result in a lot of logical repetition. So let's try to implement the dry principle with adorners.
Introduction to Adorners
If you are unfamiliar with adorners, I can simply explain that the adorner is actually a valid function wrapper, the Python interpreter loads the function and executes the wrapper, and the wrapper can modify the function's receive parameters and return values. For example, if I want to always return an integer result that is older than the actual return value, I can write the adorner like this:
# A decorator receives the method it ' s wrapping as a variable ' F ' def increment (f): # We use arbitrary args and keyword s to # Ensure we grab all the input arguments. def wrapped_f (*args, **kw): # Note We call F against the variables passed into the wrapper, # and cast the result to an int and increment. return int (f (*args, **kw)) + 1 return Wrapped_f # The wrapped function gets returned.
Now we can use the @ symbol and this decorator to decorate another function:
@incrementdef Plus (A, b): return a + b result = Plus (4, 6) assert (result = = one, "We wrote our decorator wrong!")
The adorner modifies the existing function to assign the result of the adorner's return to the variable. In this example, the result of ' plus ' actually points to the result of increment (plus).
Error returned for non-POST request
Now let's apply adorners in some of the more useful scenarios. If it is not the POST request that is received in Django, we return an error response with the adorner.
Def post_only (f): "" " ensures a method is post only" "" def Wrapped_f (request): if Request.method! = "POST":
response = HttpResponse (Json.dumps ( {"Error": "This method is only accepts posts!"}) Response.status_code = return response return F (Request) return Wrapped_f
Now we can apply this adorner in the registration API above:
@post_onlydef Register (Request): result = None try: user = User.objects.create_user (request. post[' username '), request. post[' email ', request. post[' password ') # Optional Fields for field in [' First_Name ', ' last_name ']: if field in request. POST: setattr (User, field, request.) Post[field]) user.save () result = {"Success": True} except Keyerror as e: result = {"error": Str (E)} response = HttpResponse (Json.dumps (Result)) if "error" in result: Response.status_code = return response
Now we have an adorner that can be reused in each API method.
Send JSON response
In order to send a JSON response (while processing a 500 status code), we can create another decorator:
Def json_response (f): "" " return the response as JSON, and return a of error code if an error exists" "" def Wrap PED (*args, **kwargs): result = f (*args, **kwargs) response = HttpResponse (Json.dumps (Result)) if Type ( result) = = Dict and ' error ' in result: Response.status_code = 500return response
Now we can remove the JSON-related code from the original method and add an adorner instead:
Post_only@json_responsedef Register (Request): try: user = User.objects.create_user (request. post[' username '), request. post[' email ', request. post[' password ') # Optional Fields for field in [' First_Name ', ' last_name ']: if field in request. POST: setattr (User, field, request.) Post[field]) user.save () return {"Success": True} except Keyerror as E: return {"Error": Str (E)}
Now, if I need to write a new method, I can do redundant work with adorners. If I want to write a login method, I just need to have a positive correlation code:
@post_only @json_responsedef Login (Request): If Request.user is not None: return {"Error": "User is already authenticated! "} user = Auth.authenticate (request. post[' username '), request. post[' password ') if user is not None: if not user.is_active: return {"Error": "User is inactive"} Auth.login (Request, user) return {"Success": True, "id": user.pk} Else: return {"Error": "User does not exist with those credentials "}
BONUS: Parameterize Your request method
I have used the Tubogears framework, where request parameters are interpreted directly to convey the method which I like. So how do you imitate this feature in Django? Well, adorners are a solution!
For example:
def parameterize_request (types= ("POST",)): "" " parameterize the request instead of parsing the request directly . Only the types specified is added to the query parameters. e.g. convert A=TEST&B=CV in Request. POST to F (a=test, B=CV) "" " def wrapper (f): def wrapped (request): kw = {} if ' GET ' in types:< C10/>for K, v in Request. Get.items (): kw[k] = v if "POST" in types: for K, V in Request. Post.items (): kw[k] = v return f (Request, **kw) return wrapped return wrapper
Note that this is an example of a parameterized adorner. In this example, the result of the function is the actual adorner.
Now I can write the method with parametric adorner! I can even choose whether to allow get and post, or just a request parameter type.
@post_only @json_response@parameterize_request (["post]") def register (request, username, email, password, first_ Name=none, Last_name=none): user = User.objects.create_user (username, email, password) user.first_name= First_Name user.last_name=last_name user.save () return {"Success": True}
Now we have a simple, easy-to-understand API.
BONUS #2: Save docstrings and function names using Functools.wraps
Unfortunately, one side effect of using adorners is that the method name (__name__) and docstring (__doc__) values are not saved:
def increment (f): "" " increment a function result" "" Wrapped_f (A, B): return F (A, B) + 1 return wrapped_ F @incrementdef Plus (A, b) "" "Add Things Together" "" return a + B plus.__name__ # This was now ' Wrapped_f ' Instead of ' Plus ' plus.__doc__ # this-now returns ' Increment a function result ' instead of ' Add-a-things '
This will cause problems with applications that use reflection, such as Sphinx, an application that automatically generates documents.
To solve this problem, we can use the ' wraps ' adorner to append the name and docstring:
From Functools import wraps def increment (f): "" " increment a function result" "" @wraps (f) Wrapped_f (A, B): C3/>return F (A, B) + 1 return wrapped_f @incrementdef Plus (A, b) "" "Add, Things Together" "" " return a + b PLUS.__NAME__ # This returns ' Plus ' plus.__doc__ # This returns ' Add both things together '
BONUS #3: Using ' decorator ' adorner
If you take a closer look at the way the adorner is used, there is a lot of repetition in the wrapper declaration and return place.
You can install the Python egg ' decorator ', which contains a ' decorator ' adorner that provides an adorner template!
Using Easy_install:
$ sudo easy_install decorator
or PIP:
$ pip Install decorator
Then you can simply write:
From decorator import decorator @decoratordef post_only (f, request): "" " ensures a method was post only" " if requ Est.method! = "POST": response = HttpResponse (Json.dumps ( {"Error": "This method is only accepts posts!"} ) Response.status_code = return response return F (Request)
The more awesome thing about this decorator is that it preserves the return value of __name__ and __doc__, which encapsulates the functionality of Functools.wraps!