The Python adorner is a powerful tool for eliminating redundancy. As the functionality is modular to fit, even the most complex workflows, adorners can make it a neat feature.
For example, let's look at the Django Web framework, which handles the requested method, receives a method object, and returns a response object:
def handle_request (Request): Return
HttpResponse ("Hello, World")
I recently encountered a case where you need to write several API methods that meet the following criteria:
- Return JSON response
- If it is a GET request, then return the error code
As an example of a registered API endpoint, 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 error return code in each API method. This can lead to a lot of logical repetition. So let's try to use the adorner to achieve the dry principle.
Introduction to Adorners
If you are not familiar with the adorner, I can briefly explain that, in fact, the adorner is a valid function wrapper, the Python interpreter loaded the function will execute the wrapper, the wrapper can modify the function of the receive parameters and return values. For example, if I want to always return an integer result that is greater 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 Ke Ywords to
# Ensure we grab all the input arguments.
def wrapped_f (*args, **kw):
# We call F against the variables passed to the wrapper, # and cast the result
to a int and increment.
return int (f (*args, **kw)) + 1 return
Wrapped_f # The wrapped function gets returned.
Now we can decorate another function with the @ symbol and the adorner:
@increment
def plus (A, b): return
A + b result
= Plus (4, 6)
assert (Result = = one, "We wrote our Decorat or wrong! ")
The adorner modifies the existing function and assigns the result returned by the adorner to the variable. In this example, the result of ' plus ' actually points to the result of increment (plus).
Return error for non POST request
Now let's apply the adorner in some more useful scenarios. If a POST request is not received in Django, we return an error response with the adorner.
Def post_only (f): "" "
ensures a method are 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 above registration API:
@post_only
def 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 adorner:
Def json_response (f): "" "return
to the response as JSON, and return a + error code if an error exists" ""
def Wrap PED (*args, **kwargs): Result
= f (*args, **kwargs)
response = HttpResponse (Result)
if type ( result) = = Dict and ' error ' in result:
Response.status_code = + Return
response
Now we can remove the JSON-related code from the original method and add an adorner instead:
Post_only
@json_response
def 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 the redundancy work with the adorner. If I'm going to write the login method, I just need to have the code in the photo positive:
@post_only
@json_response
def login (Request):
If Request.user isn't 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: Parameterization of Your request method
I have used the Tubogears framework, where the request parameters are interpreted directly to the forwarding method which I like very much. So how do you imitate this feature in Django? Well, the decorator is 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:< C11/>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 the parameterized 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 there are no saved method names (__name__) and docstring (__doc__) values:
def increment (f): "" "
increment a function result" "
Wrapped_f (A, B): Return
F (A, B) + 1 return
Wrapp Ed_f
@increment
def plus (A, b)
"" "Add Two things Together" "" Return
A + b
plus.__name__ # is now ' wrapped_f ' instead of ' plus '
plus.__doc__ # This now returns ' Increment a function result ' instead of ' a DD Two things together '
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 attach the name and docstring:
From Functools import wraps
def increment (f):
"" "Increment a function result" "
@wraps (f)
Wrapped_f (A , b): Return
F (A, B) + 1 return
wrapped_f
@increment
def plus (A, b)
"" "Add Two things Together" ""
return A + b
plus.__name__ # This returns ' Plus '
plus.__doc__ # This returns ' Add two things '
BONUS #3: Using the ' 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 the return place.
You can install Python egg ' decorator ', which contains a ' decorator ' adorner that provides an adorner template!
Use Easy_install:
$ sudo easy_install decorator
or PIP:
Then you can simply write:
From decorator import Decorator
@decorator
def post_only (f, request): "" "ensures a method is post only" "
if request.method!= "POST":
response = HttpResponse (Json.dumps (
{"Error": "This method is only accepts posts!"})
Response.status_code = + return
response return
F (Request)
The better thing about this decorator is that it preserves the return value of __name__ and __doc__, which encapsulates the Functools.wraps function!