最近对Pydantic的应用很感兴趣。目前的PyQt5项目中正好要用Pydantic对象去构造一些配置信息,于是思考能否用Pydantic去简化配置对话框的构造过程。预期的目标是写一个泛型的对话框基类,接收具体的"Pydantaic类"类型,跟据类类型的元信息自动地去构造相应的对话框。描述下来感觉还是比较晦涩的,阅读和运行代码反而更容易理解。下面直接给出代码:
#!/usr/bin/python3
# coding: utf-8
import sys
from PyQt5.QtWidgets import QApplication, QDialog, QGridLayout, QLabel, QComboBox, QLineEdit
from typing import TypeVar, Generic
from pydantic import BaseModel, Field
from enum import IntEnum
ModelType = TypeVar('ModelType', bound=BaseModel)
class PydanticDialog(QDialog):
def __init__(self, model: type[ModelType], parent=None):
super(QDialog, self).__init__(parent=parent)
self.model = model
self.pydantic_layout = QGridLayout()
self.pydantic_widgets = dict()
self.constructFromMetaInfo()
def constructFromMetaInfo(self):
fileds = self.model.model_fields
for row, (field_name, field_info) in enumerate(fileds.items()):
self.pydantic_layout.addWidget(QLabel(field_info.description), row, 0)
if issubclass(field_info.annotation, IntEnum):
self.pydantic_widgets[field_name] = QComboBox()
self.pydantic_widgets[field_name].addItems([item.alias for item in list(field_info.annotation)])
self.pydantic_widgets[field_name].setCurrentText(field_info.default.alias)
else:
self.pydantic_widgets[field_name] = QLineEdit()
self.pydantic_widgets[field_name].setText(str(field_info.default))
self.pydantic_layout.addWidget(self.pydantic_widgets[field_name], row, 1)
def asLayout(self):
self.setLayout(self.pydantic_layout)
class IntEnumWithAlias(IntEnum):
def __new__(cls, value, alias):
obj = int.__new__(cls, value)
obj._value_ = value
obj.alias = alias
return obj
class FruitEnum(IntEnumWithAlias):
APPLE = 0, "苹果"
ORANGE = 1, "橙子"
BANANA = 2, "香蕉"
class ItemEnum(IntEnumWithAlias):
FOO = 0, "FOO"
BAR = 1, "BAR"
class Config(BaseModel):
Ip: str = Field(alias='ip', default = "192.168.1.100", description="IP地址")
Port: int = Field(alias="port", default = 554, description="服务端口")
Mac: str = Field(alias="mac", default = "CC-F9-E4-81-77-51", description="MAC地址")
PosX: float = Field(alias="pos_x", default = 1.5, description="坐标X")
PosY: float = Field(alias="pos_y", default = 2.2, description="坐标Y")
Fruit: FruitEnum = Field(alias="fruit", default = FruitEnum.BANANA, description="水果")
Item: ItemEnum = Field(alias="item", default = ItemEnum.FOO, description="组件")
class ConfigDialog(PydanticDialog):
def __init__(self, parent=None):
super().__init__(Config, parent)
self.setWindowTitle("配置")
self.setLayout(self.pydantic_layout)
if __name__ == "__main__":
app = QApplication(sys.argv)
cd = ConfigDialog()
cd.show()
sys.exit(app.exec_())
直接运行代码,可以看到如下对话框:
上述程序中,值得解释的几点如下:
- 构造了一个PydanticDialog类,接收一个type[ModelType]类型的参数,实质上就是接收类类型,从而实现泛型(最开始尝试了用Generic[ModelType],想在代码里直接用ModelType去获取类的元信息,但发现构造PydanticDialog[Config]泛型类后,ModelType并未指向Config,因此换用了本文的赋值方案)。
- 在PydanticDialog类中,通过获取Pydantic类的model_fields字段,获取到全部Field的元信息,包括Field的名字、类型、默认值和描述。跟据这些信息就可以构造对话框了。
- 跟据Field为文本还是枚举,构造QLineEdit和QComboBox两种输入框,其中QComboBox的取值范围由枚举类获得。(注意到界面上ComboBox展示的是中文,是因为代码中给枚举加了alias这个附属字段,可参看这篇文章)。
- 最终的ConfigDialog在继承PydanticDialog的基础上,可更改Layout并增加其他控件,比如增加提交按钮等。