Build your webframework in Python
Tags: python
Published: 2019-09-30 04:08:32

---
title: Build your webframework in Python
published: true
description: 
tags: 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](https://www.python.org/dev/peps/pep-3333/). 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.

```python
# 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.

```python
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.

```python
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`.

```python
    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__`.

```python
    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.
```sh
$ python3 app.py
```

And request.
```sh
$ 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](https://github.com/mtwtkman/tomoyo) which is a little little more complex than above webframework.

Bye!