单元测试接入及pydantic的使用

Poetry 使用文档

Poetry 简介

Poetry 是一个用于管理 Python 依赖项的工具。它使用一个名为 pyproject.toml 的文件来指定项目的依赖项,并提供了一个命令行工具来安装和管理这些依赖项。

Poetry 的优势

  • 依赖锁定:Poetry通过一个poetry.lock 的文件将项目的依赖项锁定到一个特定的版本,这可以确保安装的依赖版本完全一致。

  • 依赖变更:Poetry依赖版本变更时,会同步更新pyproject.toml,无需手动处理requirements.txt

  • 依赖包管理:Poetry 可以定义不同环境的依赖,不需要再维护requirements-dev.txt、requirements-test.txt、requirements-pro.txt等多个文件。

  • 残留依赖清理:当使用pip uninstall卸载某个依赖时,其相关依赖并不会被清理,导致占用额外空间,如图

    [站外图片上传中...(image-1ab1cf-1751965482239)]

    如果使用poetry add kac-api==0.2.0安装后,通过poetry remove kac-api卸载,则可以将其关联的依赖卸载干净,这可以有效解决版本一致,但实际包内容变更的场景
    [站外图片上传中...(image-baa94e-1751965482239)]

Poetry 的安装

Poetry 可以通过以下命令安装,该命令是在虚拟环境外执行:

pip install poetry

Poetry 的使用

poetry init  # 初始化,与poetry new类似,但poetry new会创建项目文件,用于从头开始创建整个项目

在一个项目中首次使用,通过上面命令,会在项目根目录下创建一个名为 pyproject.toml 的文件,并在其中指定项目的依赖项,如下

[tool.poetry]
name = "meta"
version = "0.1.0"
description = ""
authors = ["Your Name <your@email.com>"]
[tool.poetry.dependencies]  #生产环境的依赖列表
python = "^3.6.2"
[tool.poetry.dev-dependencies] #测试环境的依赖列表
pytest = "^6.2"

Poetry 还可以用于管理项目的虚拟环境:

poetry shell # 激活项目的虚拟环境(环境不存在,自动创建)
poetry env list 查看poetry管理下的虚拟环境列表
poetry env remove venv 删除虚拟环境

然后可以通过以下命令安装项目的依赖项:

poetry install
  • --remove-untracked 移除lockfile之外的依赖,比如通过pip install安装的,高版本使用--sync

如果想要增加新的依赖可以使用poetry add或者直接修改pyproject.toml,然后执行poetry install

poetry add kac-api

当依赖发生变更后,需要更新lock文件(没有则新建),lock文件可以确保在不同环境中安装相同的依赖项版本,以提供项目的可重现性和一致性,因为poetry install时,项目会根据lock文件中的依赖版本去安装,而不是根据pyproject.toml设置的范围动态选择

poetry lock

poetry show 可以用查看所有依赖,与 pip list类似

(.venv) amiter@amiterdeMacBook-Pro kmc % poetry show | grep kac
kac-api                 0.2.0        canway kingeye kac_api
(.venv) amiter@amiterdeMacBook-Pro kmc % 

有时候可能会遇到依赖变更,但版本没变的情况,就需要清理掉缓存,否则会提示依赖冲突的报错;这种情况本身是不合理的,应该避免覆盖版本的情况,应该遵循新的SDK发布方案

poetry cache clear pypi:kac-api:0.2.0 

删除某个依赖,及其本身的依赖

poetry remove

如果是已经存在的项目且不想额外创建虚拟环境时,需要图中配置设置为true,poetry会优先在项目根目录下使用虚拟环境

[站外图片上传中...(image-2418a5-1751965482239)]

Poetry 的更多信息

https://python-poetry.org/docs/


Pytest 使用文档

简单示例

编写测试用例

在项目中创建一个名为tests的文件夹,并在其中创建一个名为test_demo.py的Python文件

def add(x, y):
    return x + y

def test_add():
    assert add(2, 3) == 5

断言

断言是测试过程中用于验证预期结果的关键部分,用于比较值、集合、异常等,如下在测试用例中使用断言方法的示例:

def test_add():
    assert add(2, 3) == 5
    
def test_list_contains_element():
    assert 2 in [1, 2, 3]

def test_monitor_source_strategy_1(data):
    # 测试数据校验
    with pytest.raises(ValidationError):
        SaveStrategy(**data).dict()

运行测试

在项目根目录下,运行以下命令:

pytest tests
  • -s 可以打印出测试函数的print输出
  • -v 显示每个测试函数的执行是否成功
  • -x 遇到执行失败立即退出

Pytest将自动发现并执行测试用例,并提供相应的输出和结果。

pytest配置方式

1.最常见的就是pytest.ini

[pytest]
testpaths = tests # 用例搜索路径
python_files = # 还可以指定类或函数级别
        tests.py
        test_*.py 
addopts = --strict-markers # 添加一些通用的额外参数
markers =  # 自定义标记,执行pytest -m slow 只执行带有此标记的用例
    slow: marks tests as slow (deselect with '-m \"not slow\"')

2.结合poetry使用,直接在pyproject.toml中定义配置即可

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["tests.py", "test_*.py"]
addopts = "--strict-markers"
markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]

...

使用场景

钩子函数

setup、teardown 分别在每条测试用例执行前和结束后执行,可以做一些数据准备,以及清理的操作

setup_class、teardown_class分别在每个测试类初始化前后执行,可以定义一些当前类中的测试方法共享的内容,而不需要每个用例都去创建

class TestDemo:
    def test_demo(self):
        print("test_demo 执行")
        assert True

    def test_demo1(self):
        print("test_demo1 执行")
        assert True

    def setup_class(cls):
        print("setup_class 执行")

    def teardown_class(cls):
        print("teardown_class 执行")

    def setup(self):
        print("setup 执行")

    def teardown(self):
        print("teardown 执行")
============================= test session starts ==============================
collecting ... collected 2 items
test_demo.py::TestDemo::test_demo 
test_demo.py::TestDemo::test_demo1 
============================== 2 passed in 6.66s ===============================
Process finished with exit code 0
setup_class 执行
setup 执行
Using existing test database for alias 'default' ('test_kmc_saas')...
PASSED                                 [ 50%]test_demo 执行 None
teardown 执行
setup 执行
PASSED                                [100%]test_demo1 执行 None
teardown 执行
teardown_class 执行

fixture使用

1.当一个函数或方法被fixture装饰后,就可以被当做测试用例或者其他fixtures的入参来使用,pytest会根据入参的名称去查找对应的fixture,找到就会执行并把执行的结果返回,如图bake_node会创建一个拨测节点实例,在函数中可以继续引用

class TestGetUptimeCheckNodeList:
    @pytest.fixture
    def bake_node(self):
        return baker.make(UptimeCheckNode, bk_biz_id=2, is_deleted=False, ip="10.11.25.115", node_id=10001)
    @pytest.mark.parametrize(...
    def test_select_uptime_check_node(
        self,
        mocker,
        esb_bk_monitor_baseurl: ft.esb_bk_monitor_baseurl,
        admin_client,
        request_params,
        hosts_msg,
        return_value,
        bake_node,
    ):
    ...

2.配合yield实现测试用例的前后执行(类似钩子函数)

如图yield前为生成fixture时执行的内容,之后则为销毁的内容

@fixture(autouse=True)
def clear_caches(db, redis_caches):
    yield
    # 清理缓存
    for cache in redis_caches.all():
        cache.clear()

3.使用autouse参数,可以使其自动执行,而无需引入,但这仅在当前module下生效,如果其他module想用,仍需要导入

@pytest.fixture(autouse=True)
def ss():
    print("aaaaa")


class TestDemo001:
    @pytest.mark.parametrize("a", [1, 2, pytest.param(3, marks=pytest.mark.skip)])
    def test_demo(self, a):
        print("test_demo 执行", a)
        assert True

    @pytest.mark.parametrize("a", [1, 2, pytest.param(3, marks=pytest.mark.skip)])
    def test_demo1(self, a):
        print("test_demo1 执行", a)
        assert True

fixture作用域

  • function(默认值):在测试函数被调用时创建,调用完成后销毁
  • class:在class中的最后一个测试函数调用完成后,fixture销毁
  • module:在module中最后一个测试函数调用完成后,fixture销毁
  • package:在package的最后一个测试函数调用完成后,fixture销毁
  • session:整个测试会话运行一次,fixture在测试session结束时被销毁

内置fixture

通过pytest --fixtures 查看所有的fixture

  • db :可以用于执行数据库的操作

  • transactional_db:可以开启事务,相当于TransactionTestCase

  • admin_client : 模拟管理员的client

  • settings :用于访问和修改 pytest 的配置选项

    @fixture(autouse=True)
    def celery_always_eager_enabled(settings):
        """开启celery的eager模式"""
        settings.CELERY_ALWAYS_EAGER = True
        yield
    
  • cache :与django的cache不是同一个,可以用来缓存和共享与测试相关数据,加快执行速度

    [站外图片上传中...(image-58887f-1751965482239)]

    当我们通过命令行执行测试后(通过pycahrm执行不会产生),就会在当前目录下生成一个.pytest_cache文件夹,该文件夹中就保存了测试相关的数据以及缓存的数据,通过pytest -cache-show也可以查看

    (.venv) amiter@amiterdeMacBook-Pro kmc % pytest --cache-show
    ================================================================================ test session starts ================================================================================
    platform darwin -- Python 3.6.8, pytest-7.0.1, pluggy-1.0.0
    benchmark: 3.4.1 (defaults: timer=time.perf_counter disable_gc=False min_rounds=5 min_time=0.000005 max_time=1.0 calibration_precision=10 warmup=False warmup_iterations=100000)
    django: settings: settings (from option)
    rootdir: /Users/amiter/PycharmProjects/kmc, configfile: pyproject.toml, testpaths: tests
    plugins: benchmark-3.4.1, Faker-14.2.1, lambda-1.2.6, mock-3.6.1, testmon-1.3.3, requests-mock-1.11.0, cov-4.0.0, django-4.5.2, lazy-fixture-0.6.3
    cachedir: /Users/amiter/PycharmProjects/kmc/.pytest_cache
    ------------------------------------------------------------------------------- cache values for '*' --------------------------------------------------------------------------------
    cache/stepwise contains:
      []
    name contains:
      'jack'
    test_cache contains:
      2
    

    pytest --cache-clear 执行测试前先清空该缓存文件

    如下是一个全局缓存的示例

    # Conftest.py
    @fixture(autouse=True, scope="session")
    def init_test_cache(request):
        request.config.cache.set("test_cache", 0)
        print(request.config.cache.get("test_cache", None), "test cache start")
        yield
        print(request.config.cache.get("test_cache", None), "test cache end")
    
    # test_demo.py
    def test_addition(cache):
        print(cache.get("test_cache", None), "test_addition cache")
        assert 2 + 2 == 4
        
    django: settings: settings (from option)
    rootdir: /Users/amiter/PycharmProjects/kmc, configfile: pyproject.toml
    collected 1 item                                                                                                                                                                    
    tests/test_demo.py::test_addition 0 test cache start
    0 test_addition cache
    PASSED0 test cache end
    =============== 1 passed in 2.91s =============================================
    

    如上图可以看到我们插入的test_cache取到了值,这里的缓存在下次执行测试时仍可使用,可以将一些计算耗时的操作进行缓存,提升测试执行速度

conftest文件

conftest.py文件是固定写法,可以共享本地固定fixture和钩子函数,一般存在于测试目录的顶层,主要作用是可以避免导入,定义在该文件的fixture都可以直接在用例中引入

@fixture(autouse=True, scope="session")
def exec_time():
    start_time = time.time()
    yield
    print(f"本次此时执行时间:{time.time()-start_time}")

跳过测试

有时候需要跳过某些测试,例如当测试依赖于特定的环境或条件时,pytest提供了多种方法来实现测试的跳过。

跳过测试的示例:

import pytest

@pytest.mark.skip(reason="跳过原因")
def test_function_to_skip():
    assert 1 == 2

@pytest.mark.skipif(1==2, reason="跳过原因")
def test_function_to_skip_if_condition():
    assert 1 == 2
    

也可以用pytest -m 去指定某些标记,去执行,例如 pytest -m "slow"

@pytest.mark.slow
def test_slow():
    pass

参数化测试

@pytest.mark.parametrize

  • 使用不同的输入参数在单个测试函数中多次运行相同的测试代码

class TestGetUptimeCheckTaskList:

    @classmethod
    @pytest.fixture
    def create_task(cls):
        """数据库插入两条数据"""
        return [
            baker.make(
                UptimeCheckTask, task_id=i["id"], protocol=i["protocol"], config=i["config"], bk_biz_id=i["bk_biz_id"]
            )
            for i in cls.return_value["data"]
        ]

    @pytest.mark.parametrize(
        "query_params, return_value, result",
        [
            ({"url": "http://www.baidu.com"}, return_value, [10001]),  # 存在的url
            ({"url": "http://www.xxxxx.com"}, return_value, []),  # 不存在的url
            ({}, return_value, [10001, 10002]),  # 不传参数,全部返回
        ],
    )
    def test_format_task_params(
        self,
        backend_esb_client: ft.backend_esb_client,
        esb_bk_monitor_baseurl: ft.esb_bk_monitor_baseurl,
        faker: ft.faker,
        query_params,
        return_value,
        result,
        create_task,
        mocker,
    ):
        mocker.patch(
            "home_application.views.monitor_scene.uptime_task_mgmt.search_uptime_check_task_list",
            return_value=(2, return_value["data"]),
        )
        # 准备数据
        viewset = UptimeCheckTaskViewSet()

        class MockRequest(object):
            ...

        viewset.request = MockRequest()
        viewset.request.query_params = query_params

        task_alarm_map = {
            "10001_total": 10001,
            "10001__alarm_level": "remind",
            "10002_total": 10002,
            "10002__alarm_level": "remind",
        }
        queryset_tuple = [(task, "蓝鲸", task_alarm_map) for task in UptimeCheckTask.objects.values()]
        ret_data = MultiArgsThreadPool().map(viewset.format_task_params, queryset_tuple)
        ret_data = list(filter(lambda x: x, ret_data))
        assert len(ret_data) == len(result)
        assert [i["task_id"] for i in ret_data] == result

在上面的例子中,test_format_task_params函数将使用不同的参数组合进行三次运行。

  • @pytest.mark.parametrize可以用于装饰整个测试类,并将所有所有参数传入

  • @pytest.mark.parametrize可以多次装饰同一个测试方法,测试方法将会根据参数个数做笛卡尔积生成不同的参数组合,下面的测试函数将会执行9次

    import pytest
    
    @pytest.mark.parametrize("bk_biz_id", [1, 2, 3])
    @pytest.mark.parametrize("task_name", ["tcp001", "tcp002", "tcp003"])
    def test_get_task_list(bk_biz_id, task_name):
        ...
    
  • @pytest.mark.parametrize可以对某个具体的参数进行标记

    import pytest
    
    @pytest.mark.parametrize("task_name,["tcp001",pytest.param("tcp002",marks=pytest.mark.skip)])
    def test_search_task_detail(bk_biz_id, task_name):
        ...
    ============================= test session starts ==============================
    collecting ... collected 3 items
    test_demo.py::TestDemo001::test_demo[1] 
    test_demo.py::TestDemo001::test_demo[2] 
    test_demo.py::TestDemo001::test_demo[3] 
    ========================= 2 passed, 1 skipped in 6.81s =========================
    ...
    Using existing test database for alias 'default' ('test_kmc_saas')...
    PASSED                           [ 33%]test_demo 执行 1
    PASSED                           [ 66%]test_demo 执行 2
    SKIPPED (unconditional skip)     [100%]
    Skipped: unconditional skip
    本次执行时间:6.623747110366821
    

makers使用

makers用于对测试用例进行分类和控制测试的行为,可以使用 pytest --markers 查看所有的标记

支持多选,假设按测试用例的重要性做了分级, pytest tests -vs -m "p0 and p1"表示被这两个标记的

  • 常用的内置标记

    • skip:跳过测试用例,不执行它。
    • xfail:标记测试用例为预期失败,即如果测试用例失败,不会报告为错误。
    • parametrize:允许为测试用例参数化,以便多次运行同一个测试用例的不同参数组合。
    • fixture:指定一个测试用例所需的固定的测试环境或预置条件。
    • usefixtures:指定一个测试用例使用指定的fixture。
    • pylint:标记测试用例为需要进行静态代码分析(例如使用Pylint)的测试。
    • timeout:为测试用例设置一个超时时间,如果测试用例执行时间超过指定的时间,将会被中断。
    • repeat:指定一个测试用例需要被重复执行的次数。
  • 自定义标记示例

  • slow :可以直接在pyproject.toml中定义makers

    [tool.pytest.ini_options]
    testpaths = ["tests"]
    python_files = ["tests.py", "test_*.py"]
    addopts = "--strict-markers"
    markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
    

如下,当使用pytest tests -vs -m "slow"时,pytest只执行了被标记的test_demo方法,如果需要执行没被标记的,只需pytest tests -vs -m "not slow"

class TestDemo:
    @pytest.mark.slow #使用了slow标记
    def test_demo(self):
        print("test_demo 执行")
        assert True

    def test_demo1(self):
        print("test_demo1 执行")
        assert True

(.venv) amiter@amiterdeMacBook-Pro kmc % pytest tests -vs -m slow
========================================================================= test session starts =========================================================================
platform darwin -- Python 3.6.8, pytest-7.0.1, pluggy-1.0.0 -- /Users/amiter/PycharmProjects/kmc/.venv/bin/python
cachedir: .pytest_cache
rootdir: /Users/amiter/PycharmProjects/kmc, configfile: pyproject.toml, testpaths: tests
collected 2 items / 1 deselected / 1 selected                                                                                                                         
tests/test_demo.py::TestDemo
test_demo 执行
PASSED

mocker使用

mocker是pytest-mock插件提供的,基于unittest中mock模块,用于模拟单测中的一些方法的行为和返回内容,且语法更简单,无需显示创建Mock实例

  • 模拟函数的返回值

        def test_search_event_by_page(
                self,
                backend_esb_client: ft.backend_esb_client,
                esb_bk_monitor_baseurl: ft.esb_bk_monitor_baseurl,
                faker: ft.faker,
                alarm_event,
                mocker,
        ):
            # mock search_event的返回值
            mocker.patch(
                "home_application.tasks.search_event",
                return_value={
                    "16968544551223039": {"status": "CLOSED", "end_time": "2023-10-09 21:06:53"},
                },
            )
            # mock 异步方法
            mocker.patch("home_application.tasks.push_event_to_kac")
    
            search_event_by_page([(alarm_event.bk_biz_id, alarm_event.alarm_id, alarm_event.begin_time)])
    
            assert AlarmEvent.objects.filter(alarm_id=alarm_event.alarm_id).exists()
    
  • 模拟函数的行为

    class MyClass:
        def get_value(self):
            return 42
    
    def test_get_value(mocker):
        my_obj = MyClass()
        mocker.patch.object(my_obj, "get_value").return_value = 100
        result = my_obj.get_value()
        assert result == 100
    
     @pytest.mark.parametrize(
            "biz_strategy_data_map,strategy_item_id,return_value",
            [
                (biz_strategy_data_map_system, 1, [return_value1]),
                (biz_strategy_data_map_system, 1, [return_value2, return_value1]),
            ],
        )
        def test_create_or_update(self, biz_strategy_data_map, strategy_item_id, return_value, mocker):
            mocker.patch(    "kmc.monitor_template.service.distribution_strategy_service.DistributionStrategyService.client"
                ".monitor_v3.save_alarm_strategy_v2",
                side_effect=return_value)
    
      这里要注意当return_value 和 side_effect同时定义,side_effect生效
    

注意点

1.以类的方式编写测试用例时,类中不能定义__init__方法,否则pytest无法搜集到该类下的用例

2.开启数据库访问 pytestmark = pytest.mark.django_db

3.mocker的导入路径,当要mock的对象是从其他模块导入的时候,patch中的路径要声明为当前模块,例如b模块中引用了from a import search_event

mocker.patch("home_application.a.search_event") ❌

mocker.patch("home_application.b.search_event") ✅


pydantic使用

pydantic优点

  • 数据解析和序列化

    将原始数据(如 JSON、字典等)转换为 Pydantic 模型的实例。同时,它还支持将 Pydantic 模型转换为 JSON、字典等其他格式的数据,方便数据的序列化和反序列化

  • 声明式数据校验

    通过类型注解和校验器定义字段类型,自动校验和转换数据,确保数据类型及格式符合预期,避免因参数类型导致bug

  • 性能占优

    def pydantic_performance_test():
        model = CreateUptimeCheckTask(**data)
        return model.dict()
    def drf_serializer_performance_test():
        serializer = UptimeCheckTaskSerializer(data=data)
        serializer.is_valid()
        return serializer.validated_data
    pydantic_time = timeit.timeit(pydantic_performance_test, number=10000)
    drf_serializer_time = timeit.timeit(drf_serializer_performance_test, number=10000)
    print(f"Pydantic performance: {pydantic_time} seconds")
    print(f"Drf Serializer performance: {drf_serializer_time} seconds")
    Pydantic performance: 0.6493919999338686 seconds
    Drf Serializer performance: 10.693156832945533 seconds
    
  • 文档生成

快速使用

模型定义

1.pydantic中定义对象的主要方法是通过继承类BaseModel

from pydantic import BaseModel,Field
from typing import List
class SwitchStrategy(BaseModel):
    ids: List[int] = Field(description="策略ID列表")
    is_enabled: bool = Field(description="是否启用")
    
SwitchStrategy(ids=[1,"2"],is_enabled=True).dict() # StrictInt
Out[20]: {'ids': [1, 2], 'is_enabled': True}

pydantic 默认会对传入数据的类型做转换,上面SwitchStrategy的ids我传了字符串,最后输出的是int,如果不想要强制转换,pydantic还提供了严格类型,StrictInt、StrictStr、StrictBytes、StrictInt、StrictFloat、StrictBool

  • dict() : 将一个模型实例转换为字典

    • exclude_unset 默认 False
    • exclude_none 默认 False
    • exclude_defaults 默认 False
    • exclude 返回字典中包含的字段
    • include 返回字典中不包含的字段
    • by_alias 是否将字段别名用作返回字典中的键
  • json() : 将一个模型实例转换为json,参数与上面一致

  • copy() :复制模型

    • deep 是否深拷贝

      # 默认shallow
      inst = SwitchStrategy(ids=[1,"2"],is_enabled=True)
      inst_copy_shallow = inst.copy()
      inst.ids
      Out[100]: [1, 2]
      inst.ids.append(3)
      inst.ids
      Out[102]: [1, 2, 3]
      inst_copy_shallow.ids
      Out[103]: [1, 2, 3]
      # deep
      inst = SwitchStrategy(ids=[1,"2"],is_enabled=True)
      inst_copy_deep = inst.copy(deep=True)
      inst.ids
      Out[105]: [1, 2]
      inst.ids.append(4)
      inst.ids
      Out[107]: [1, 2, 4]
      inst_copy_deep.ids
      Out[108]: [1, 2]
      
    • update 更新复制后的字典

      SwitchStrategy(ids=[1,"2"],is_enabled=True).copy(update={"ids":[3,4]}).dict()
      Out[82]: {'ids': [3, 4], 'is_enabled': True}
      
  • parse_obj():跟SwitchStrategy(**{"ids":[1,2,3]})类似,对参数要求为dict,keyword参数会报错

    class Plugin(BaseModel):
        os_type: str = Field(default="")
        plugin_id: str = Field(default="")
    plugin = Plugin()
    p_dict = plugin.dict()
    p_json = plugin.json()
    plugin1 = Plugin.parse_obj(p_dict)
    plugin == plugin1
    Out[75]: True
    plugin2 = Plugin.parse_raw(p_json)
    plugin == plugin2
    Out[77]: True
    plugin1 = Plugin.parse_obj(**p_dict)
    Traceback (most recent call last):
      File "/Users/amiter/PycharmProjects/kmc/.venv/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 3343, in run_code
        exec(code_obj, self.user_global_ns, self.user_ns)
      File "<ipython-input-79-474933518c21>", line 1, in <module>
        plugin1 = Plugin.parse_obj(**p_dict)
      File "pydantic/main.py", line 513, in pydantic.main.BaseModel.parse_obj
    TypeError: parse_obj() takes exactly 2 positional arguments (1 given)
    
  • parse_raw():跟SwitchStrategy(**{"ids":[1,2,3]})类似,对参数要求为json参数

  • construct():创建模型而不运行validator

validator

与drf-serializer类似,pydantic也提供对字段的单个校验和全部校验

  • 单个字段的校验
class HttpUptimeCheckTaskConfig(BaseUptimeCheckTaskConfig):
    @validator("method")
    def validate_method(cls, value):
        choices = [("GET", "GET"), ("POST", "POST"), ("PUT", "PUT"), ("PATCH", "PATCH"), ("DELETE", "DELETE")]
        if value not in [k for k, v in choices]:
            raise ValueError("invalid uptime check method")
        return value
  • validator复用
  
  def validate_status(value):
      if value not in [v for k, v in Status.__dict__.items() if not k.startswith("__")]:
          raise ValueError("invalid uptime check task status")
      return value
  
  class ChangeUptimeCheckTaskStatus(BaseModel):
      _validate_status: ClassVar[Callable] = validator("status", allow_reuse=True)(validate_status)
      task_id: int = Field(description="任务ID")
      status: str = Field(description="任务状态")
  
  
  class ChangeUptimeCheckTaskStatusData(BaseModel):
      _validate_status: ClassVar[Callable] = validator("status", allow_reuse=True)(validate_status)
      id: int = Field(description="任务ID")
      status: str = Field(description="任务状态")
  • each_item使用(相当于for循环)
class A(BaseModel):
    names:List[str]
    @validator('names', each_item=True)
    def check_names_not_empty(cls, v):
        assert v != '', 'empty strings are not allowed.'
        return v
    
A(names=["qq",""])
Traceback (most recent call last):
  File "/Users/amiter/PycharmProjects/kmc/.venv/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 3343, in run_code
    exec(code_obj, self.user_global_ns, self.user_ns)
  File "<ipython-input-27-02207c301f6c>", line 1, in <module>
    A(names=["qq",""])
  File "pydantic/main.py", line 341, in pydantic.main.BaseModel.__init__
pydantic.error_wrappers.ValidationError: 1 validation error for A
names -> 1
  empty strings are not allowed. (type=assertion_error)

注意:each_item不在从父类继承到的列表属性中使用,如下

class _A(BaseModel):
    names:List[str]
    
class A(_A):
    @validator('names', each_item=True)
    def check_names_not_empty(cls, v):
        assert v != '', 'empty strings are not allowed.'
        return v
    
A(names=["qq",""])
Out[31]: A(names=['qq', ''])
  • 整个模型验证
class RemoteCollectingSlz(BaseModel):
    @root_validator(pre=True)
    def check_remote_collecting_slz(cls, attrs: Dict) -> Dict:
        assert "bk_host_id" in attrs or ("ip" in attrs and "bk_cloud_id" in attrs), "主机id和ip/bk_cloud_id不能同时为空"
        return attrs

    ip: Optional[str] = Field(description="远程采集服务器IP")
    bk_cloud_id: Optional[int] = Field(description="蓝鲸云区域ID")
    bk_host_id: Optional[int] = Field(description="主机ID")
    bk_supplier_id: Optional[int] = Field(description="开发商账号")
    is_collecting_only: bool = Field(description="是否只采集数据")

Model Config

可以通过Config模型来修改pydantic的行为

class Plugin(BaseModel):
    @validator("plugin_id")
    def validate_plugin_id(cls, v):
        return v
    class Config:
        use_enum_values = True
        validate_all = True
        extra = Extra.allow
        smart_union = True

    os_type: OsType = Field(default=OsType.linux.value)
    plugin_id: str = Field(default="")
    
from tewanwan import *
plugin = Plugin(strategy_id=12)
plugin.dict()
Out[4]: {'os_type': 'linux', 'plugin_id': '','strategy_id':12}
  • validate_all : 是否验证字段默认值,默认False,如下item_id默认写了字符串,并不会报错;

    class Item(BaseModel):    
        item_id:int = "hello"
        class Config:
            copy_on_model_validation = "shallow"
            # validate_all = True
    class Strategy(BaseModel):
        item: Item
    item = Item()
    strategy = Strategy(item=item)
    
      另外要注意如果在模型validator中定义了不存在就赋值的逻辑(创建时间,默认用户等),就必须validate_all设       置为True
    
  • use_enum_values:填充模型时是否采用枚举的属性,默认False,如果使用了枚举类型,建议开启

    class OsType(str, Enum):
        linux = "linux"
        windows = "windows"
    class Plugin(BaseModel):
        os_type: OsType = Field(default=OsType.linux.value)
        plugin_id: str = Field(default="")
        
    Plugin().dict()
    Out[57]: {'os_type': <OsType.linux: 'linux'>, 'plugin_id': ''}
    Plugin.Config.use_enum_values = True
    Plugin().dict()
    Out[59]: {'os_type': 'linux', 'plugin_id': ''}
    
  • extra:初始化期间是否忽略(ignore)、允许(allow)或禁止(forbid)额外属性,默认是ignore''

  • smart_union:避免union中的数据类型强转,默认False

    from tewanwan import *
    Plugin(task_id=12).dict()
    Out[2]: {'os_type': 'linux', 'plugin_id': '', 'task_id': 12}
    Plugin.Config.smart_union = False
    Plugin(task_id=12).dict()
    Out[4]: {'os_type': 'linux', 'plugin_id': '', 'task_id': '12'}
    
  • copy_on_model_validation:控制模型实例复制的方式,1.9.1版本默认深拷贝

    • 'none'- 不做任何处理
    • 'shallow'- 默认是浅拷贝
    • 'deep'- 深拷贝
    from pydantic import BaseModel
    
    class Item(BaseModel):
          item_id:int = 0
        class Config:
            copy_on_model_validation = "none"
    
    class Strategy(BaseModel):
        item: Item
    
    item = Item()
    strategy = Strategy(item=item)
    assert strategy.item.item_id == 0
    assert strategy.item.item_id is item.item_id
    assert strategy.item is item
    # Passes
    Item.Config.copy_on_model_validation="shallow"
    item = Item()
    strategy = Strategy(item=item)
    assert strategy.item is item
    Traceback (most recent call last):
      File "/Users/amiter/PycharmProjects/kmc/.venv/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 3343, in run_code
        exec(code_obj, self.user_global_ns, self.user_ns)
      File "<ipython-input-9-1a73d0a9a45e>", line 3, in <module>
        assert strategy.item is item
    AssertionError
    # Failed
    
  • arbitrary_types_allowed: 是否允许用户自定义类型

    from pydantic import BaseModel
    
    class Item():
         def __init__(self, item_id: str):
            self.item_id = item_id
        
    class Strategy(BaseModel):
        item: Item
        class Config:
            arbitrary_types_allowed = True
    strategy = Strategy(item=Item(item_id='a_test'))
    
  • validate_assignment:属性赋值时是否执行校验,默认为False,除非确定实例属性类型正确,否则建议开启

    from pydantic import BaseModel
    class Item(BaseModel):
        item_id:int = 0
    class Strategy(BaseModel):
        item: Item
        
    item = Item(item_id=10)
    item.item_id = {"sss"}
    strategy= Strategy(item=item)
    strategy.dict()
    Out[38]: {'item': {'item_id': {'sss'}}}
    Item.Config.validate_assignment = True
    item = Item(item_id=11)
    item.item_id = {"sss"}
    Traceback (most recent call last):
      File "/Users/amiter/PycharmProjects/kmc/.venv/lib/python3.6/site-packages/IPython/core/interactiveshell.py", line 3343, in run_code
        exec(code_obj, self.user_global_ns, self.user_ns)
      File "<ipython-input-41-d648006e3959>", line 1, in <module>
        item.item_id = {"sss"}
      File "pydantic/main.py", line 380, in pydantic.main.BaseModel.__setattr__
    pydantic.error_wrappers.ValidationError: 1 validation error for Item
    item_id
      value is not a valid integer (type=type_error.integer)
    

注意点

模型之间继承后,序列化时会获取最先符合条件的模型

from pydantic import BaseModel as bs
from typing import Union
class A(bs):
    a: int
class B(A):
    b: int
class C(bs):
    c: Union[B, A]
C(c={"a":1,"b":2}).dict()
Out[15]: {'c': {'a': 1}}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容