「译文」如何编写 Python Web 框架(二)

简介: 「译文」如何编写 Python Web 框架(二)

「译文」如何编写 Python Web 框架(一) ,我们开始编写自己的 Python 框架并实现以下功能:

  • WSGI 兼容
  • 请求处理程序
  • 路由:简单和参数化

请务必在此之前阅读系列的 「译文」如何编写 Python Web 框架(一)

这部分同样令人兴奋,我们将在其中添加以下功能:

  • 检查重复的路径
  • 基于类的处理程序
  • 单元测试

Ready? 让我们开始吧。

重复的路径

现在,我们的框架允许添加任意次数相同的路由。因此,以下内容将起作用:

@app.route("/home")
def home(request, response):
    response.text = "Hello from the HOME page"
@app.route("/home")
def home2(request, response):
    response.text = "Hello from the SECOND HOME page"
PYTHON

框架不会抱怨,因为我们使用 Python 字典来存储路由,只有最后一个才能使用 http://localhost:8000/home/。显然,这并不好。我们希望确保框架在用户尝试添加现有路由时会抛出信息。您可以想象,实施起来并不是很困难。因为我们使用 Python dict 来存储路由,所以我们可以简单地检查字典中是否已存在给定路径。如果是,我们抛出异常,如果不是,我们让它添加一个路由。在我们编写任何代码之前,让我们回忆下我们的主要API 类:

# 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.items():
            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.text = "Not found."
PYTHON

我们需要更改 route 函数,以便在再次添加现有路由时抛出异常:

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

现在,尝试添加相同的路径两次并重新启动你的 gunicorn。您应该看到抛出以下异常:

Traceback (most recent call last):
...
AssertionError: Such route already exists.
VIM

我们可以重构它以将其减少到一行:

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

完工!进入下一个功能。

基于类的处理程序

如果你了解 Django,你知道它支持基于函数和基于类的视图(即我们的处理程序)。我们已经有了基于函数的处理程序。现在我们将添加基于类的,适用于更复杂, 更大的处理程序。我们基于类的处理程序将如下所示:

# app.py
@app.route("/book")
class BooksHandler:
    def get(self, req, resp):
        resp.text = "Books Page"
    def post(self, req, resp):
        resp.text = "Endpoint to create a book"
    ...
PYTHON

这意味着我们存储路径的 dict: self.routes可以包含类和函数作为值。因此,当我们在 handle_request() 方法中找到一个处理程序时,我们需要检查处理程序是一个函数还是一个类。如果它是一个函数,它应该像现在一样工作。如果它是一个类,根据请求方法,我们应该调用该类的对应方法。也就是说,如果请求方法是 GET,我们应该调用类的get() 方法,如果是 POST 我们应该调用 post 方法等。这是 handle_request() 方法现在的样子:

# 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
PYTHON

我们要做的第一件事是检查找到的处理程序是否是一个类。为此,我们使用 inspect 模块:

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

现在,如果正在使用基于类的处理程序,我们需要根据请求方法找到类的适当方法。为此,我们可以使用内置的 getattr 函数:

# api.py
def handle_request(self, request):
    response = Response()
    handler, kwargs = self.find_handler(request_path=request.path)
    if handler is not None:
        if inspect.isclass(handler):
            handler_function = getattr(handler(), request.method.lower(), None)
            pass
        else:
            handler(request, response, **kwargs)
    else:
        self.default_response(response)
    return response
PYTHON

getattr接受一个对象实例作为第一个参数,将属性名称作为第二个参数。第三个参数是如果没有找到则返回的值。因此,GET将返回 getPOST 返回 post, some_other_attribute 返回 None。如果handler_functionNone,则表示此类函数未在类中实现,并且不允许此请求方法:

if inspect.isclass(handler):
    handler_function = getattr(handler(), request.method.lower(), None)
    if handler_function is None:
        raise AttributeError("Method not allowed", request.method)
PYTHON

如果实际找到了 handler_function,那么我们只需调用它:

if inspect.isclass(handler):
    handler_function = getattr(handler(), request.method.lower(), None)
    if handler_function is None:
        raise AttributeError("Method now allowed", request.method)
    handler_function(request, response, **kwargs)
PYTHON

现在整个方法看起来像这样:

def handle_request(self, request):
    response = Response()
    handler, kwargs = self.find_handler(request_path=request.path)
    if handler is not None:
        if inspect.isclass(handler):
            handler_function = getattr(handler(), request.method.lower(), None)
            if handler_function is None:
                raise AttributeError("Method now allowed", request.method)
            handler_function(request, response, **kwargs)
        else:
            handler(request, response, **kwargs)
    else:
        self.default_response(response)
PYTHON

我不喜欢我们有两个 handler_functionhandler。我们可以重构它们以使它更优雅:

def handle_request(self, request):
    response = Response()
    handler, kwargs = self.find_handler(request_path=request.path)
    if handler is not None:
        if inspect.isclass(handler):
            handler = getattr(handler(), request.method.lower(), None)
            if handler is None:
                raise AttributeError("Method now allowed", request.method)
        handler(request, response, **kwargs)
    else:
        self.default_response(response)
    return response
PYTHON

就是这样。我们现在可以测试对基于类的处理程序的支持。首先,如果你还没有, 请将此处理程序添加到app.py

@app.route("/book")
class BooksHandler:
    def get(self, req, resp):
        resp.text = "Books Page"
PYTHON

现在,重新启动你的 gunicorn 并转到页面 http://localhost:8000/book,你应该看到消息Books Page。就这样, 我们增加了对基于类的处理程序的支持。可以试试实现其他方法(例如postdelete)。

进入下一个功能!

单元测试

如果没有单元测试,哪个项目是可靠的,对吧?所以让我们添加几个。我喜欢使用pytest,所以让我们安装它:

pip install pytest
SHELL

并创建一个文件,我们将编写测试:

touch test_bumbo.py
SHELL

提醒一下,bumbo是框架的名称。您可能以不同的方式命名。另外,如果您不知道 pytest 是什么,我强烈建议您查看它以了解如何编写单元测试。

首先,让我们为我们的 API 类创建一个我们可以在每个测试中使用的工具:

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

现在,对于我们的第一次单元测试,让我们从简单的开始。让我们测试一下我们是否可以添加路径。如果它没有抛出异常,则表示测试成功通过:

def test_basic_route(api):
    @api.route("/home")
    def home(req, resp):
        resp.text = "YOLO"
PYTHON

像这样运行测试:pytest test_bumbo.py你应该看到如下内容:

collected 1 item
test_bumbo.py .                                                                                                                                                            [100%]
====== 1 passed in 0.09 seconds ======
ABNF

现在,让我们测试它是否会在我们尝试添加现有路由时抛出异常:

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

再次运行测试,您将看到它们都通过了。

我们可以添加更多测试,例如默认响应,参数化路由,状态代码等。但是,所有测试都要求我们向处理程序发送 HTTP 请求。为此,我们需要一个测试客户端。但是如果我们在这里做的话,我认为这篇文章会变得太大了。我们将在这些系列的下一篇文章中完成。我们还将添加对模板和其他一些有趣内容的支持。所以,请继续关注。

像往常一样,如果您想看一些功能实现,请在评论部分告诉我。

P.S. 这些博客文章基于我正在构建的 Python Web 框架。因此, 请在这儿 查看博客中的内容,一定要通过 star 该 repo 来表达你的喜爱。

Fight on!

相关文章
|
18天前
|
安全 前端开发 数据库
Python 语言结合 Flask 框架来实现一个基础的代购商品管理、用户下单等功能的简易系统
这是一个使用 Python 和 Flask 框架实现的简易代购系统示例,涵盖商品管理、用户注册登录、订单创建及查看等功能。通过 SQLAlchemy 进行数据库操作,支持添加商品、展示详情、库存管理等。用户可注册登录并下单,系统会检查库存并记录订单。此代码仅为参考,实际应用需进一步完善,如增强安全性、集成支付接口、优化界面等。
|
2月前
|
安全 关系型数据库 测试技术
学习Python Web开发的安全测试需要具备哪些知识?
学习Python Web开发的安全测试需要具备哪些知识?
41 4
|
7天前
|
JSON 安全 中间件
Python Web 框架 FastAPI
FastAPI 是一个现代的 Python Web 框架,专为快速构建 API 和在线应用而设计。它凭借速度、简单性和开发人员友好的特性迅速走红。FastAPI 支持自动文档生成、类型提示、数据验证、异步操作和依赖注入等功能,极大提升了开发效率并减少了错误。安装简单,使用 pip 安装 FastAPI 和 uvicorn 即可开始开发。其优点包括高性能、自动数据验证和身份验证支持,但也存在学习曲线和社区资源相对较少的缺点。
42 15
|
4天前
|
关系型数据库 API 数据库
Python流行orm框架对比
Python中有多个流行的ORM框架,如SQLAlchemy、Django ORM、Peewee、Tortoise ORM、Pony ORM、SQLModel和GINO。每个框架各有特点,适用于不同的项目需求。SQLAlchemy功能强大且灵活,适合复杂项目;Django ORM与Django框架无缝集成,易用性强;Peewee轻量级且简单,适合小型项目;Tortoise ORM专为异步框架设计;Pony ORM查询语法直观;SQLModel结合Pydantic,适合FastAPI;GINO则适合异步环境开发。初学者推荐使用Django ORM或Peewee,因其易学易用。
|
7天前
|
人工智能 分布式计算 大数据
MaxFrame 产品评测:大数据与AI融合的Python分布式计算框架
MaxFrame是阿里云MaxCompute推出的自研Python分布式计算框架,支持大规模数据处理与AI应用。它提供类似Pandas的API,简化开发流程,并兼容多种机器学习库,加速模型训练前的数据准备。MaxFrame融合大数据和AI,提升效率、促进协作、增强创新能力。尽管初次配置稍显复杂,但其强大的功能集、性能优化及开放性使其成为现代企业与研究机构的理想选择。未来有望进一步简化使用门槛并加强社区建设。
43 7
|
1月前
|
JSON 数据可视化 测试技术
python+requests接口自动化框架的实现
通过以上步骤,我们构建了一个基本的Python+Requests接口自动化测试框架。这个框架具有良好的扩展性,可以根据实际需求进行功能扩展和优化。它不仅能提高测试效率,还能保证接口的稳定性和可靠性,为软件质量提供有力保障。
73 7
|
1月前
|
分布式计算 大数据 数据处理
技术评测:MaxCompute MaxFrame——阿里云自研分布式计算框架的Python编程接口
随着大数据和人工智能技术的发展,数据处理的需求日益增长。阿里云推出的MaxCompute MaxFrame(简称“MaxFrame”)是一个专为Python开发者设计的分布式计算框架,它不仅支持Python编程接口,还能直接利用MaxCompute的云原生大数据计算资源和服务。本文将通过一系列最佳实践测评,探讨MaxFrame在分布式Pandas处理以及大语言模型数据处理场景中的表现,并分析其在实际工作中的应用潜力。
83 2
|
1月前
|
敏捷开发 测试技术 持续交付
自动化测试之美:从零开始搭建你的Python测试框架
在软件开发的马拉松赛道上,自动化测试是那个能让你保持节奏、避免跌宕起伏的神奇小助手。本文将带你走进自动化测试的世界,用Python这把钥匙,解锁高效、可靠的测试框架之门。你将学会如何步步为营,构建属于自己的测试庇护所,让代码质量成为晨跑时清新的空气,而不是雾霾中的忧虑。让我们一起摆脱手动测试的繁琐枷锁,拥抱自动化带来的自由吧!
|
2月前
|
开发框架 搜索推荐 数据可视化
Django框架适合开发哪种类型的Web应用程序?
Django 框架凭借其强大的功能、稳定性和可扩展性,几乎可以适应各种类型的 Web 应用程序开发需求。无论是简单的网站还是复杂的企业级系统,Django 都能提供可靠的支持,帮助开发者快速构建高质量的应用。同时,其活跃的社区和丰富的资源也为开发者在项目实施过程中提供了有力的保障。
|
2月前
|
开发框架 JavaScript 前端开发
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势
TypeScript 是一种静态类型的编程语言,它扩展了 JavaScript,为 Web 开发带来了强大的类型系统、组件化开发支持、与主流框架的无缝集成、大型项目管理能力和提升开发体验等多方面优势。通过明确的类型定义,TypeScript 能够在编码阶段发现潜在错误,提高代码质量;支持组件的清晰定义与复用,增强代码的可维护性;与 React、Vue 等框架结合,提供更佳的开发体验;适用于大型项目,优化代码结构和性能。随着 Web 技术的发展,TypeScript 的应用前景广阔,将继续引领 Web 开发的新趋势。
53 2