In a previous article I illustrated how we could put Python's function annotations to good use to provide use with a syntactically elegant way to describe which kind of parameter content would be acceptable to our simple web application framework. In this article the actual implementation is presented along with some notes on its usage.
A simple Python application server with full argument checking
The applicationserver
module provides a ApplicationRequestHandler
class that derives from the Python provided BaseHTTPRequestHandler
class. For now all we do is provide a do_GET()
method, i.e. we don't bother with HTTP POST methods yet, although it wouldn't take too much effort if we would.
The core of the code is presented below:
class ApplicationRequestHandler(BaseHTTPRequestHandler): def do_GET(self): url=urlsplit(self.path) path=url.path.strip('/') parts=path.split('/') if parts[0]=='': parts=['index'] ob=self.application try: for p in parts: if hasattr(ob,p): ob=getattr(ob,p) else: raise AttributeError('unknown path '+ url.path) except AttributeError as e: self.send_error(404,str(e)) return if ('return' in ob.__annotations__ and issubclass(ob.__annotations__['return'],IsExposed)): try: kwargs=self.parse_args(url.query,ob.__annotations__) except Exception as e: self.send_error(400,str(e)) return try: result=ob(**kwargs) except Exception as e: self.send_error(500,str(e)) return else: self.send_error(404,'path not exposed'+ url.path) return self.wfile.write(result.encode()) @staticmethod def parse_args(query,annotations): kwargs=defaultdict(list) if query != '': for p in query.split('&'): (name,value)=p.split('=',1) if not name in annotations: raise KeyError(name+' not annotated') else: kwargs[name].append(annotations[name](value)) for k in kwargs: if len(kwargs[k])==1: kwargs[k]=kwargs[k][0] return kwargsThe first thing we do in the
do_GET()
method is splitting the path into separate components. The path is provided in the path
member and is already stripped of hostname and query parameters. We strip from it any leading or trailing slashes as well (line 5). If there are no path components we will look for a method called index
.
The next step is to check each path component and see if its a member of the application we registered with the handler. If it is, we retrieve it and check whether the next component is a member of this new object. If any of these path components is missing we raise an error (line 16).
If all parts of the path can be resolved as members, we check whether the final path points to an executable with a return allocation. If this return allocation is defined and equal to our IsExposed
class (line 22) we are willing to execute it, otherwise we raise an error.
The next step is to check each argument, so we pass the query part of the URL to the static parse_args()
method that will return us a dictionary of values if all values checked out ok. If so we call the method that we found earlier with these arguments and if all went well, write its result to the output stream that will deliver the content to the client.
The parse_args()
method is not very complicated: It creates a default dictionary whose default will be an empty list. This way we can create a list of values if the query consists of more than one part with the same name. Then we split the query on the & character (line 44) and split each part in a name and a value part (these are separated by a = character). Next we check if the name is present in the annotations
dictionary and if not raise a KeyError
.
If we did find the name in the annotations
dictionary its associated value should be an executable that we pass the value from the query part (line 48). The result of this check (or conversion) is appended to the list in the default dictionary. If the value in the annotation is not an executable an exception will be raised that will not be caught. Likewise will any exception within this callable bubble up to the calling code. The final lines (line 50-53) reduce those entries in the default dictionary that consist of a list with just a single item to just that item before returning.
Conclusion
Python function annotations can be used for many purposes and this example code show a rather elegant (I think) example that let's us specify in clear language what we expect of functions that are part of web applications, which makes it quite easy too catch many input errors in an way that can be adapted to almost any input pattern.