Within the first part, we began writing our personal Python framework and applied the next options:

  • WSGI appropriate
  • Request Handlers
  • Routing: easy and parameterized

Make sure that to learn Part I of those sequence earlier than this one.

This half will likely be no much less thrilling and we are going to add the next options in it:

  • Test for duplicate routes
  • Class Primarily based Handlers
  • Unit checks

Prepared? Let’s get began.

Duplicate routes

Proper now, our framework permits so as to add the identical route any variety of occasions. So, the next will work:

@app.route("/dwelling") def dwelling(request, response):     response.textual content = "Hey from the HOME web page"   @app.route("/dwelling") def home2(request, response):     response.textual content = "Hey from the SECOND HOME web page" 

The framework won’t complain and since we use a Python dictionary to retailer routes, solely the final one will work when you go to http://localhost:8000/dwelling/. Clearly, this isn’t good. We wish to make it possible for the framework complains if the person tries so as to add an present route. As you’ll be able to think about, it isn’t very troublesome to implement. As a result of we’re utilizing a Python dict to retailer routes, we are able to merely test if the given path already exists within the dictionary. If it does, we throw an exception, if it doesn’t we let it add a route. Earlier than we write any code, let’s keep in mind our principal API class:

# api.py  class API:     def __init__(self):         self.routes = {}      def route(self, path):         def wrapper(handler):             self.routes[path] = handler             return handler          return wrapper      def __call__(self, environ, start_response):         request = Request(environ)          response = self.handle_request(request)          return response(environ, start_response)      def find_handler(self, request_path):         for path, handler in self.routes.gadgets():             parse_result = parse(path, request_path)             if parse_result is not None:                 return handler, parse_result.named          return None, None      def handle_request(self, request):         response = Response()          handler, kwargs = self.find_handler(request_path=request.path)          if handler is not None:             handler(request, response, **kwargs)         else:             self.default_response(response)          return response      def default_response(self, response):         response.status_code = 404         response.textual content = "Not discovered." 

We have to change the route operate in order that it throws an exception if an present route is being added once more:

# api.py  def route(self, path):     if path in self.routes:         throw AssertionError("Such route already exists.")      def wrapper(handler):         self.routes[path] = handler         return handler      return wrapper 

Now, attempting including the identical route twice and restart your gunicorn. It’s best to see the next exception thrown:

Traceback (most up-to-date name final): ... AssertionError: Such route already exists. 

We are able to refactor it to lower it to 1 line:

# api.py  def route(self, path):     assert path not in self.routes, "Such route already exists."      ... 

Voilà! Onto the following characteristic.

Class Primarily based Handlers

If you recognize Django, you recognize that it helps each operate primarily based and sophistication primarily based views (our handlers). We have already got operate primarily based handlers. Now we are going to add class primarily based ones that are extra appropriate if the handler is extra difficult and greater. Our class primarily based handlers will appear like this:

# app.py  @app.route("/ebook") class BooksHandler:     def get(self, req, resp):         resp.textual content = "Books Web page"      def submit(self, req, resp):         resp.textual content = "Endpoint to create a ebook"      ... 

It implies that our dict the place we retailer routes self.routes can comprise each courses and features as values. Thus, once we discover a handler within the handle_request() methodology, we have to test if the handler is a operate or if it’s a class. If it’s a operate, it ought to work similar to now. If it’s a class, relying on the request methodology, we should always name the suitable methodology of the category. That’s, if the request methodology is GET, we should always name the get() methodology of the category, whether it is POST we should always name the submit methodology and and so forth. Right here is how the handle_request() methodology seems like now:

# api.py  def handle_request(self, request):     response = Response()      handler, kwargs = self.find_handler(request_path=request.path)      if handler is not None:         handler(request, response, **kwargs)     else:         self.default_response(response)      return response 

The very first thing we are going to do is test if the discovered handler is a category. For that, we use the examine module like this:

# api.py  import examine  ...  def handle_request(self, request):     response = Response()      handler, kwargs = self.find_handler(request_path=request.path)      if handler is not None:         if examine.isclass(handler):             move   # class primarily based handler is getting used         else:             handler(request, response, **kwargs)     else:         self.default_response(response)      return response  ... 

Now, if a category primarily based handler is getting used, we have to discover the suitable methodology of the category relying on the request methodology. For that we are able to use the getattr built-in operate:

# api.py  def handle_request(self, request):     response = Response()      handler, kwargs = self.find_handler(request_path=request.path)      if handler is not None:         if examine.isclass(handler):             handler_function = getattr(handler(), request.methodology.decrease(), None)             move         else:             handler(request, response, **kwargs)     else:         self.default_response(response)      return response 

getattr accepts an object occasion as the primary param and the attribute title to get because the second. The third argument is the worth to return if nothing is discovered. So, GET will return get, POST will return submit and some_other_attribute will return None. If the handler_function is None, it implies that such operate was not methodology within the class and that this request methodology is just not allowed:

if examine.isclass(handler):     handler_function = getattr(handler(), request.methodology.decrease(), None)     if handler_function is None:         increase AttributeError("Methodology now allowed", request.methodology) 

If the handler_function was really discovered, then we merely name it:

if examine.isclass(handler):     handler_function = getattr(handler(), request.methodology.decrease(), None)     if handler_function is None:         increase AttributeError("Methodology now allowed", request.methodology)     handler_function(request, response, **kwargs) 

Now the entire methodology seems like this:

def handle_request(self, request):     response = Response()      handler, kwargs = self.find_handler(request_path=request.path)      if handler is not None:         if examine.isclass(handler):             handler_function = getattr(handler(), request.methodology.decrease(), None)             if handler_function is None:                 increase AttributeError("Methodology now allowed", request.methodology)             handler_function(request, response, **kwargs)         else:             handler(request, response, **kwargs)     else:         self.default_response(response) 

I do not like that we’ve got each handler_function and handler. We are able to refactor them to make it extra elegant:

def handle_request(self, request):     response = Response()      handler, kwargs = self.find_handler(request_path=request.path)      if handler is not None:         if examine.isclass(handler):             handler = getattr(handler(), request.methodology.decrease(), None)             if handler is None:                 increase AttributeError("Methodology now allowed", request.methodology)          handler(request, response, **kwargs)     else:         self.default_response(response)      return response 

And that is it. We are able to now check the help for sophistication primarily based handlers. First, if you have not already, add this handler to app.py:

@app.route("/ebook") class BooksResource:     def get(self, req, resp):         resp.textual content = "Books Web page" 

Now, restart your gunicorn and go to the web page http://localhost:8000/ebook and you must see the message Books Web page. And there you go. We have now added help for sophistication primarily based handlers. Play with them slightly bit by implementing different strategies resembling submit and delete as effectively.

Onto the following characteristic!

Unit Exams

What undertaking is dependable if it has no unit checks, proper? So let’s add a pair. I like utilizing pytest, so let’s set up it:

and create a file the place we are going to write our checks:

Simply to remind you, bumbo is the title of the framework. You’ll have named it otherwise. Additionally, if you do not know what pytest is, I strongly advocate you have a look at it to know how unit checks are written beneath.

To start with, let’s create a fixture for our API class that we are able to use in each check:

# test_bumbo.py import pytest  from api import API   @pytest.fixture def api():     return API() 

Now, for our first unit check, let’s begin with one thing easy. Let’s check if we are able to add a route. If it would not throw an exception, it implies that the check passes efficiently:

def test_basic_route(api):     @api.route("/dwelling")     def dwelling(req, resp):         resp.textual content = "YOLO" 

Run the check like this: pytest test_bumbo.py and you must see one thing like the next:

collected 1 merchandise  test_bumbo.py .                                                                                                                                                            [100%]  ====== 1 handed in 0.09 seconds ====== 

Now, let’s check that it throws an exception if we attempt to add an present route:

# test_bumbo.py  def test_route_overlap_throws_exception(api):     @api.route("/dwelling")     def dwelling(req, resp):         resp.textual content = "YOLO"      with pytest.raises(AssertionError):         @api.route("/dwelling")         def home2(req, resp):             resp.textual content = "YOLO" 

Run the checks once more and you will note that each of them move.

We are able to add much more checks such because the default response, parameterized routing, standing codes and and so forth. Nevertheless, all of them require that we ship an HTTP request to our handlers. For that we have to have a check consumer. However I believe this submit will develop into too huge if we do it right here. We’ll do it within the subsequent submit in these sequence. We will even add help for templates and a few different attention-grabbing stuff. So, keep tuned.

P.S. These weblog posts are primarily based on the Python web framework that I’m constructing. So, check it out to see what’s but to return within the weblog and ensure to point out some love by starring the repo.

Struggle on!