DEV Community

mtwtkman
mtwtkman

Posted on • Edited on

Build your webframework in Python

There are many really nice webframeworks written by Python in this world.
You know these webframeworks on WSGI, so let's building your original webframework now!

If you want to know the detail about WSGI spec, please reference PEP3333. This post doesn't care any details, just implementing.

Write our application

Luckly, you have already wsgi server defined at wsgiref module. All you need is creating an WSGI application entrypoint.

A most simple application is like below.

# app.py
from wsgiref.simple_server import make_server


def app(environ, start_response):
    start_response('200 OK', headers=[('Content-Type', 'text/plain:charset=utf-8')])
    response = [b'my toy app']
    return response


with make_server('', 8088, app) as server:
    server.serve_forever()

Hmm, this is so boring. Typically an webframework must have http request handlers and routing systems. So I should have these.
At first, I should think about the way to using in the real world.

from wsgiref.simple_server import make_server


things = {x: x.upper()  for x in ('a', 'b', 'c')}


def get_all_things(request):
    return ','.join(things.keys())


def create_things(request):
    # do something
    return 'ok'


class App:
    def get(self, path, handler):
        # TODO
        return self

    def post(self, path, handler):
        # TODO
        return self

    def __call__(self, environment, start_response):
        # TODO
        return response

    def start(self):
        with make_server('', 8088, self) as server:
            server.serve_forever()


app = App().get('/things', get_all_things).post('/things', create_things)
app.start()

This webframework has

  • Routing
  • Querystring
  • Path parameter
  • Only GET and POST handlers
  • App class includes WSGI server starter

and doesn't have

  • Middleware
  • Any error handlings
  • Staticfile serving
  • Template
  • Flexible content type
  • Redirect
  • More, more, more..

Overview

This is implementing example.

from urllib.parse import parse_qs
from collections import defaultdict
import re
from wsgiref.simple_server import make_server


class App:
    handler_map = defaultdict(dict)

    def _register(self, method_name, path, handler):
        self.handler_map[fr'^{path}$'].update({method_name: handler})
        return self

    def get(self, path, handler):
        return self._register('get', path, handler)

    def post(self, path, handler):
        return self._register('post', path, handler)

    def _find_handler(self, path, method):
        for pattern, info in self.handler_map.items():
            matched = re.match(pattern, path)
            if matched:
                return info[method], matched.groupdict()

    def __call__(self, environment, start_response):
        method = environment['REQUEST_METHOD'].lower()
        path = environment['PATH_INFO']
        status = {
            'get': '200 OK',
            'post': '201 Created',
        }[method]
        start_response(status, headers=[('Content-Type', 'text/plain:charset=utf-8')])
        request_body = {
            'get': lambda: environment['QUERY_STRING'],
            'post': lambda: environment['wsgi.input'].read(int(environment.get('CONTENT_LENGTH', 0))).decode(),
        }[method]
        handler, path_params = self._find_handler(path, method)
        response = handler(parse_qs(request_body), **path_params)
        return [response.encode()]

    def start(self):
        with make_server('', 8088, self) as server:
            server.serve_forever()

Routing paths and mapping handlers are important things.

App memories routing paths and corresponding handlers in handler_map.

See _register.

    def _register(self, method_name, path, handler):
        self.handler_map[fr'^{path}$'].update({method_name: handler})
        return self

    def get(self, path, handler):
        return self._register('get', path, handler)

    def post(self, path, handler):
        return self._register('post', path, handler)

This is a just method wrapper against each http method.
So App retrieves the handler corresponding requested path.

I choice re module to detect path like Django etc.

Handling request is in __call__.

    def __call__(self, environment, start_response):
        method = environment['REQUEST_METHOD'].lower()
        path = environment['PATH_INFO']
        status = {
            'get': '200 OK',
            'post': '201 Created',
        }[method]
        start_response(status, headers=[('Content-Type', 'text/plain:charset=utf-8')])
        request_body = {
            'get': lambda: environment['QUERY_STRING'],
            'post': lambda: environment['wsgi.input'].read(int(environment.get('CONTENT_LENGTH', 0))).decode(),
        }[method]()
        handler, path_params = self._find_handler(path, method)
        response = handler(parse_qs(request_body), **path_params)
        return [response.encode()]

__call__ does just simple processes.

  • fetches an http method
  • fetches a request path
  • builds a request body corresponding the http method
  • fetches a handler
  • calls a handler

OK, now I get my own toy webframework! Try to run.

Start server.

$ python3 app.py

And request.

$ curl localhost:8088
$ curl localhost:8088/things
$ curl localhost:8088/things/a
$ curl -X POST -d 'x=X' localhost:8088/things

At last, I created tiny toy not productive webframework named tomoyo which is a little little more complex than above webframework.

Bye!

Top comments (1)

Collapse
 
yokotobe profile image
yokotobe

nice article !