Как написать свой web-framework на Python

Fullstack CTO
4 min readJan 4, 2020

--

January 04, 2020

Пишем свой Flask

И так, это продолжение темы про то, как изучить Python за выходные (новогодние выходные, если что).

В отличие от PHP, в Python сервер нужно реализовывать самому. Но, фреймворки все это делают за вас, а для продакшена используются готовые решения, такие как gunicorn.

Gunicorn это вариация WSGI HTTP сервера. Что такое WSGI — оставлю на самостоятельное обучение. Чтобы добиться совместимости с WSGI, нам нужен вызываемый объект (функция или класс), который принимает два параметра (environ и start_response), и возвращает совместимый с WSGI ответ.

Предполагается что вы уже знаете про виртуальное окружение, у вас стоит последняя версия питона и вы умеете пользоваться пакетным менеджером pip. Я не учу питону, я делюсь своим опытом как написать что-то похожее на фреймворк Flask.

И так, чтобы сделать простейший вебсервер на Python достаточно создать простейший файл main.py со следующим кодом:

def app(environ, start_response):
response_body = b"Hello, World!"
status = "200 OK"
start_response(status, headers=[])
return iter([response_body])

И далее самый простой способ запустить это приложение:

gunicorn -w 1 main:app

В данном случае мы запускаем сервер с 1м воркером и указываем файл и вызываемую функцию. Все.

Но для отладки лучше использовать вебсервер из пакета wsgiref:

from wsgiref.simple_server import make_server
server = make_server('127.0.0.1', 8080, app)
server.serve_forever()

и для старта такого сервера не нужен gunicorn, достаточно просто вызвать наш main.py

Ну что же, с сервером вроде бы разобрались. Давайте создадим myflask.py и начнем разработку. Вообще схема моего проекта будет выглядеть так:

app.py
config.py
main.py
myflask.py

Редактируем app.py

from config import Config
from app import app
if __name__ == "__main__":
app.run(debug=True)

Это самый простой файл, пока у нас нет реализаций других файлов.

В данном файле мы говорим что импортируем некоторый app из файла app.py. Если этот файл вызван напрямую, а не импортирован, то мы вызываем наше приложение app.run()

Config.py

Простой файл:

class Config:
HOST = "127.0.0.1"
PORT = 8080

Создаем app.py

Я буду краток на слова и сразу показывать код. Вроде бы все так просто, что нет смысла комментировать:

from config import Config
from myflask import MyFlask
app = MyFlask()
app.config.from_object(Config)
@app.route("/")
def home():
return "Home page"
@app.route("/about")
def about():
return "This is page about MyFlask framework :)"
@app.route("/foo/{slug}")
def foo(slug):
return "Route with dinamyc {slug}"

Здесь мы создаем объект app и описываем маршрутизацию, использую классную фичу языка — декораторы.

Создаем MyFlask

Ну и теперь создаем наш фреймворк:

from webob import Request, Response
from parse import parse
import os
import re
class MyFlask:
def __init__(self):
self.__routes = {}
self.config = MyFlaskConfig()
def __call__(self, environ, start_response=None):
request = Request(environ)
response = self.handle_request(request)
return response(environ, start_response)
def run(self, debug=False):
from wsgiref.simple_server import make_server
port = int(self.config['PORT']) or 5000
host = self.config['HOST'] or "127.0.0.1"
server = make_server(host, port, self)
print(f'http://{host}:{port}')
server.serve_forever()
def route(self, path):
def wrapper(handler):
self.__routes[path] = handler
return handler
return wrapper
def handle_request(self, request):
response = Response()
handler, kwargs = self.find_handler(request_path=request.path)
if handler is not None:
response.text = handler(**kwargs)
else:
self.default_response(response)
return response
def default_response(self, response):
response.status_code = 404
response.text = "<center><h1>Not found 404</h1><hr></center>"
def find_handler(self, request_path):
for path, handler in self.__routes.items():
parse_result = parse(path, request_path)
if parse_result is not None:
return handler, parse_result.named
return None, None

Я не знаю надо ли расписывать весь этот код. Чего в этом коде не хватает — это реализации MyFlaskConfig, который описывается так:

class MyFlaskConfig:
def __init__(self):
self.__config = {
"DEBUG": False,
"HOST": "127.0.0.1",
"PORT": 5000,
}
def __getitem__(self, item):
return self.get(item)
def __setitem__(self, key, value):
self.__config[key] = value
def get(self, item, defval=None):
if item in self.__config:
return self.__config[item]
return defval
def from_object(self, obj):
for key in dir(obj):
if key.isupper():
self[key] = getattr(obj, key)

Кстати, код функции from_object подсмотрел в исходниках Flask.

И так, это минимум, который позволяет реализовать простейший фреймворк похожий по API на Flask.

Чтобы не возвращать просто текст, давайте создадим простое подобие шаблонизатора:

def render_template(tpl_file: str, **kwargs):
template_dir = os.path.dirname(__file__) + '/templates'
tpl_path = template_dir + tpl_file
with open(tpl_path) as f:
s = f.read()
for k in kwargs:
s = re.sub(r'{' + k + '}', kwargs[k], s)
return s

Теперь создадим простой html шаблон templates/index.html:

<!doctype html>
<html lang="en">
<head>
<title>{title}</title>
</head>
<body>
{content}
</body>
</html>

Теперь наш app.py можно переписать так:

from config import Config
from myflask import MyFlask, render_template
app = MyFlask()
app.config.from_object(Config)
@app.route("/")
def home():
list_str = "<ul>"
list_str += list_str.join([f'<li><a href="/hello/Item_{i}">Item {i}</a></li>' for i in range(4)]) + "</ul>"
return render_template('/index.html',
title="Index",
content=f"<h1>Main content</h1><hr>{list}",
)
@app.route("/about")
def about():
return "This is page about MyFlask framework :)"
@app.route("/foo/{slug}")
def foo(slug):
return render_template('/index.html',
title="Hello",
content=f"<h1>Hello, {slug}!</h1>",
)

Тут показан пример как сделать наипростейший шаблонизатор. И вот когда что-то такое я написал для себя, мне стало как-то проще. Еще я до кучи почитал исходники Flask и теперь готов написать что-то в продакшн. Как вариант, для начала начну с административной панели GeekJob.ru — тем более архитектура SOA, так что могу смело добавлять сервисы в текущий проект без боли.

Запуск в production

Если хочется запустить это в продакшен, то создаем файл конфигурации для gunicorn:

# gunicorn_config.pycommand = "/Users/mayorov/workspace/pyprojects/myflask/main.py"
pythonpath = "/Users/mayorov/workspace/pyprojects/myflask/.venv/bin/python"
bind = '127.0.0.1:8000'
workers = 17 # количество ядер * 2 + 1
worker_class = 'sync'
worker_connections = 1000
timeout = 30
keepalive = 2
user = "www"
# raw_env = "тут переменные окружения"

Создаем скрипт запуска bin/start_gunicorn.sh:

#!/usr/bin/env bashsource  /Users/mayorov/workspace/pyprojects/myflask/.venv/bin/activate
exec gunicorn -c "/Users/mayorov/workspace/pyprojects/myflask/gunicorn_config.py" main:app

Ну и теперь запускаем наш проект через:

/Users/mayorov/workspace/pyprojects/myflask/bin/start_gunicorn.sh

Ну вот примерно как-то так работает Flask. Конечно его возможности сильно богаче, я только показал утрированный пример. В качестве шаблонизатора надо бы прикрутить Jinja2. Реализовать логику блюпринтов… Ну и много чего еще сделать, на самом деле. Но у меня нет сил и желания все разжевывать досконально. Для обучения в самый раз. Если было полезно — ставьте лайк.

Итого

Мне понравился Python как язык, особенно возможность перегрузки операторов. Много математических функций из коробки, ну и сам язык имеет свои приятные черты. Далее по плану чисто ради фана пройти где-нибудь собеседование, понять на что я тяну ?

--

--

Fullstack CTO
Fullstack CTO

Written by Fullstack CTO

CTO and co-founder at NEWHR & Geekjob

No responses yet