[日常学习] -2025-8-18- 页面元类和装饰器工厂
一、搞清楚页面类的元类
class ElementMeta(type):
def __new__(cls, name, base, attr):
attr['element'] = None
attr['parent'] = None
......
return super().__new__(cls, name, base, attr)
1. class ElementMeta(type):
- 作用:定义一个名为
ElementMeta
的元类。 - 为什么继承
type
?因为在 Python 中,type
是所有类的 “元类”(比如int
、str
、你自己写的类,本质上都是type
创建的)。自定义元类必须继承type
,才能拥有创建其他类的能力。 - 类比:如果普通类是 “造房子的图纸”,元类就是 “画图纸的模板”—— 它决定了 “图纸”(类)长什么样。
2. def __new__(cls, name, base, attr):
- 作用:这是元类的核心方法,负责 “创建新的类”。当你用
ElementMeta
创建其他类时,会自动调用这个方法。 - 参数解释:
cls
:指ElementMeta
自己(就像普通方法里的self
,但这里是元类本身)。name
:要创建的 “新类的名字”(比如你写的HistoryPageBase
,这个参数就是"HistoryPageBase"
)。base
:新类的 “父类们”(比如HistoryPageBase
继承PortalPage
,这个参数就是(PortalPage,)
)。attr
:一个字典,存放新类的 “属性和方法”(比如类里定义的danpingxinzengLoc
属性、__init__
方法等,都存在这里)。
3. attr['element'] = None
- 作用:给 “即将被创建的新类” 强制添加一个名为
element
的属性,初始值为None
。 - 举例:如果用
ElementMeta
创建HistoryPageBase
,那么HistoryPageBase
会自动有一个element
属性,默认是None
。 - 用途:
element
通常用来存储 “页面元素的实例”(比如某个按钮、输入框的具体对象),方便后续操作。
4. attr['parent'] = None
- 作用:和上一行类似,给新类添加
parent
属性,初始值为None
。 - 用途:
parent
一般用来记录 “当前类的父元素 / 父页面”(比如一个按钮的父容器是某个 div,一个子页面的父页面是首页),方便管理元素之间的层级关系。
5. from frameworkCore.business.runContext import RunContext
- 作用:导入一个叫
RunContext
的工具类。 - 用途:
RunContext
通常是框架里管理 “运行环境” 的类(比如当前用的浏览器驱动、是否本地运行、测试报告配置等),后面的逻辑可能会用到它。
6. if name != 'ElementBase':
- 作用:判断 “即将创建的新类的名字” 是不是
ElementBase
。如果不是,才执行后面的逻辑。 - 为什么?因为
ElementBase
可能是 “最基础的元素类”(直接用这个元类创建),而其他类(比如PortalPage
、HistoryPageBase
)都是ElementBase
的子类。这里的逻辑是:只给子类添加额外功能,基础类ElementBase
保持简单。
7. #if not RunContext.is_run_local():
- 作用:这是一行注释掉的代码,原本的意思是 “如果不是本地运行(比如在服务器上跑测试)”,才执行下面的逻辑。
- 现在注释掉了,可能是暂时用不到,或者留着以后扩展。
8. for attr_name, attr_value in attr.items():
- 作用:遍历新类的所有 “属性和方法”(
attr
是个字典,attr_name
是名字,attr_value
是具体的值或方法)。 - 举例:如果新类有
__init__
方法、查询
方法,这里就会逐个遍历这些方法。
9. if (type(attr_value).__name__ == 'method' or type(attr_value).__name__ == 'function') and not attr_name.startswith('__') and not attr_name.endswith('__') and attr_name!='':
- 作用:筛选出需要 “特殊处理” 的方法。条件拆解:
type(attr_value)
是method
或function
:只处理 “方法 / 函数”(不处理普通属性,比如element
、locator
)。not attr_name.startswith('__') and not attr_name.endswith('__')
:排除 Python 自带的 “特殊方法”(比如__init__
、__str__
,这些方法名前后带双下划线)。attr_name!=''
:排除空名字的方法(实际开发中很少见,保险用)。
10. attr[attr_name] = show_case_step()(attr_value)
- 作用:用
show_case_step()
这个 “装饰器” 包装筛选出来的方法。 - 装饰器的作用:可以在不修改原方法代码的情况下,给方法添加额外功能。比如
show_case_step()
可能是用来 “记录测试步骤” 的 —— 当方法执行时(比如点击按钮、输入文本),自动打印日志(如 “执行了【查询】方法”),方便调试和生成测试报告。
11. return super().__new__(cls, name, base, attr)
- 作用:调用父类(
type
)的__new__
方法,按照上面修改后的attr
(添加了element
、parent
属性,包装了方法),正式创建并返回这个新类。
总结:这个元类到底干了啥?
简单说,所有用ElementMeta
创建的类(你的页面类都是它的 “后代”)都会:
- 自动带上
element
(元素实例)和parent
(父级对象)两个基础属性,统一管理元素关系。 - 除了最基础的
ElementBase
类,其他子类的普通方法(比如 “查询”“点击”)都会被show_case_step()
装饰,自动记录操作步骤。
这样做的目的是让所有页面类保持统一的基础结构,同时自动添加测试步骤记录功能,减少重复代码,让框架更规范。
二、搞清楚这个元类里面的装饰器
⭐ show_case_step()
从哪里来?
show_case_step()
不是 Python 内置的装饰器,而是框架开发者在项目内部定义的工具函数,通常放在框架的 “工具模块”(比如 frameworkCore/utils/decorators.py
或 frameworkCore/business/step.py
这类文件中)。
⭐ 装饰器的基础语法(先理解 “什么是装饰器”)
装饰器本质是 “一个包装其他函数的函数”,作用是在不修改原函数代码的情况下,给函数添加额外功能(比如日志、计时、权限校验等)。
1. 最简单的装饰器语法(无参数)
定义一个装饰器的基本结构:
# 定义装饰器(本质是一个函数,参数是被装饰的函数)
def 装饰器名(被装饰的函数):def 包装函数(*args, **kwargs): # *args, **kwargs 用来接收被装饰函数的参数# 步骤1:函数执行前的操作(比如打印开始日志)print(f"开始执行:{被装饰的函数.__name__}")# 步骤2:执行原函数(核心逻辑)结果 = 被装饰的函数(*args, **kwargs)# 步骤3:函数执行后的操作(比如打印结束日志)print(f"执行结束:{被装饰的函数.__name__}")return 结果 # 返回原函数的结果return 包装函数 # 返回包装后的函数
使用装饰器(用 @装饰器名
放在函数定义上方):
@装饰器名 # 等价于:函数名 = 装饰器名(函数名)
def 测试函数():print("执行核心逻辑")# 调用函数时,会自动触发装饰器的额外功能
测试函数()
# 输出:
# 开始执行:测试函数
# 执行核心逻辑
# 执行结束:测试函数
2. show_case_step()
的语法(带括号的装饰器,可能带参数)
你代码中用的是 show_case_step()(attr_value)
,注意装饰器名后有 ()
,这说明它是 **“带参数的装饰器”**(或 “装饰器工厂”)—— 先调用 show_case_step(可能的参数)
生成一个 “实际的装饰器”,再用这个装饰器包装函数。
它的定义逻辑大致如下(框架内部可能这样实现):
# 定义“装饰器工厂”(带参数的装饰器)
def show_case_step(步骤描述=None): # 可以接收参数,比如“步骤描述”# 内部定义实际的装饰器def 实际装饰器(被装饰的函数):def 包装函数(*args, **kwargs):# 步骤1:记录步骤开始(比如打印到日志、写入测试报告)# 这里可能会获取函数名、参数、步骤描述等信息step_name = 步骤描述 or 被装饰的函数.__name__ # 用函数名当默认描述print(f"【步骤开始】{step_name}")# 步骤2:执行原函数(比如“点击按钮”“输入文本”的核心逻辑)结果 = 被装饰的函数(*args, **kwargs)# 步骤3:记录步骤结束print(f"【步骤结束】{step_name}")return 结果return 包装函数 # 返回包装后的函数return 实际装饰器 # 返回实际的装饰器
⭐ show_case_step()
在框架中怎么用?
在你的 UI 自动化框架中,它的用法有两种场景:
1. 元类中自动应用(你代码中的场景)
元类 ElementMeta
会自动给所有页面类的普通方法(比如 “查询”“点击”“输入”)套上 show_case_step()
装饰器:
# 元类中这行代码的作用:
# 给页面类的方法(比如HistoryPageBase的“查询”方法)自动加上装饰器
attr[attr_name] = show_case_step()(attr_value)# 等价于:
# 假设页面类有个“查询”方法
def 查询(self, data):print("执行查询逻辑")# 元类会自动处理为:
查询 = show_case_step()(查询)
当你调用 页面实例.查询(data)
时,会自动触发装饰器的步骤记录:
history_page = HistoryPageBase(...)
history_page.查询("测试数据")
# 装饰器会自动输出类似:
# 【步骤开始】查询
# 执行查询逻辑
# 【步骤结束】查询
2. 手动给方法加装饰器(框架中可能的用法)
如果需要给特定方法添加自定义步骤描述,开发者可能手动使用:
class HistoryPageBase(PortalPage):@show_case_step(步骤描述="展开面板并输入查询条件") # 手动指定步骤描述def 查询(self, data):self.展开.expand()self.搜索框.填充数据(data)
调用时会输出:
【步骤开始】展开面板并输入查询条件
【步骤结束】展开面板并输入查询条件
三、详解装饰器
def show_case_step(): # 外层函数:装饰器工厂(无参数)def __fun(fun): # 中层函数:接收被装饰的目标函数def warps(*args, **kwargs): # 内层函数:实际执行的包装函数# 核心逻辑:记录步骤 + 执行目标函数 + 处理异常return warps # 返回包装函数return __fun # 返回中层函数
- 调用流程:
@show_case_step()
→ 等价于目标函数 = show_case_step()(__fun)(目标函数)
→ 最终执行的是warps
函数。 - 作用:通过三层嵌套,实现 “在目标函数执行前后添加额外逻辑(步骤记录、异常处理)”。
1. 获取运行上下文(run_ctx
)
run_ctx = RunContext.getRunContext()
RunContext
是框架中的 “运行上下文管理器”,存储了当前测试的全局信息(如是否显示步骤、浏览器驱动、日志配置等)。run_ctx
就像一个 “全局变量包”,让装饰器能访问测试运行的各种配置。
2. 判断是否需要显示步骤
if run_ctx and run_ctx.run_modal_config is not None and run_ctx.run_modal_config.show_case_step and run_ctx.driver:
- 条件解读:只有当 “存在运行上下文、配置有效、开启了步骤显示功能、存在浏览器驱动” 时,才执行步骤记录。
- 目的:通过配置控制是否显示步骤(比如调试时显示,正式运行时不显示),灵活适配不同场景。
3. 生成步骤信息并更新显示
# 获取当前步骤(可能来自上下文或新生成)
step = run_ctx.current_step if hasattr(run_ctx, 'current_step') else get_case_step_info()
# 生成步骤显示内容(函数名、参数等)
show_content = get_fun_infos(fun, *args)
# 安全更新步骤显示(忽略更新失败的异常)
with contextlib.suppress(Exception):run_ctx.driver.update_label_content(step, show_content)
step
:表示当前测试步骤的标识(可能是步骤 ID、序号等,用于定位显示位置)。show_content
:通过get_fun_infos
生成要显示的内容(比如 “执行了 HistoryPageBase. 查询方法,参数为:{'data': ' 测试数据 '}”)。update_label_content
:通过浏览器驱动更新步骤显示(可能是在测试报告页面、控制台或日志中展示)。contextlib.suppress(Exception)
:确保 “步骤显示失败” 不会影响目标函数的核心逻辑(比如驱动异常时,不阻断测试执行)。
warps
函数的后半部分是核心亮点:捕获目标函数的异常,统一包装为可跟踪的异常,方便测试问题定位
1. 判断是否开启日志模式
if run_ctx and run_ctx.run_modal_config.open_logger:
- 只有开启日志模式时,才执行复杂的异常处理(否则直接执行函数,不记录异常细节)。
2. 执行目标函数并捕获异常
try:# 执行被装饰的目标函数(比如“查询”“点击”等核心操作)return fun(*args, **kwargs)
except TrackedException as te:# 如果是已包装的“可跟踪异常”,直接抛出(不重复处理)raise te
except Exception as exc:# 处理其他所有未捕获的异常(核心逻辑)...
3. 包装普通异常为TrackedException
这部分是异常处理的核心,目的是给原始异常添加 “上下文信息”(如函数名、调用栈、参数等),方便定位问题:
- 关键工具类作用:
ExceptionCoordinator
:异常协调器,避免同一异常被多个装饰器重复处理(通过exc_id
跟踪状态)。TrackedException
:框架自定义的异常类,比普通异常多了 “上下文信息” 和 “原始异常”,方便调试(比如能看到 “哪个函数在什么参数下出错了”)。logger
和write_debug
:记录异常日志和调试信息,用于事后分析。
4. 非日志模式:直接执行
else:# 非日志模式直接执行目标函数,不做异常包装(轻量模式)return fun(*args, **kwargs)
四、装饰器工厂
元类里面的attr[attr_name] = show_case_step()(attr_value)
这句话的核心作用是:在元类创建类的过程中,给类中的方法动态应用show_case_step
装饰器,让这些方法自动获得 “步骤记录” 和 “异常处理” 的功能。
1. show_case_step()
:调用装饰器工厂,得到 “实际的装饰器”
show_case_step
是一个 “装饰器工厂”(外层函数),调用它(show_case_step()
)会返回其内部定义的__fun
函数 —— 这才是真正用来 “包装方法” 的装饰器。
把骨架粘贴过来对照着看~
def show_case_step(): # 外层函数:装饰器工厂(无参数)def __fun(fun): # 中层函数:接收被装饰的目标函数def warps(*args, **kwargs): # 内层函数:实际执行的包装函数# 核心逻辑:记录步骤 + 执行目标函数 + 处理异常return warps # 返回包装函数return __fun # 返回中层函数
把解释粘贴过来对照这个看~
# 定义“装饰器工厂”(带参数的装饰器)
def show_case_step(步骤描述=None): # 可以接收参数,比如“步骤描述”# 内部定义实际的装饰器def 实际装饰器(被装饰的函数):def 包装函数(*args, **kwargs):# 步骤1:记录步骤开始(比如打印到日志、写入测试报告)# 这里可能会获取函数名、参数、步骤描述等信息step_name = 步骤描述 or 被装饰的函数.__name__ # 用函数名当默认描述print(f"【步骤开始】{step_name}")# 步骤2:执行原函数(比如“点击按钮”“输入文本”的核心逻辑)结果 = 被装饰的函数(*args, **kwargs)# 步骤3:记录步骤结束print(f"【步骤结束】{step_name}")return 结果return 包装函数 # 返回包装后的函数return 实际装饰器 # 返回实际的装饰器
2. show_case_step()(attr_value)
:用实际的装饰器包装目标方法
attr_value
是类中定义的一个方法(比如HistoryPageBase
中的 “查询” 方法)。
用第一步得到的__fun
(实际装饰器)去包装attr_value
(即__fun(attr_value)
),会返回一个被包装后的新方法(也就是show_case_step
内部的warps
函数)。
3. attr[attr_name] = ...
:更新类的方法字典,替换原方法
attr
是元类中存储 “类的属性和方法” 的字典(比如attr
里可能有"查询": 查询方法
这样的键值对)。
把第二步得到的 “被包装后的新方法” 重新赋值给attr[attr_name]
,相当于用 “带装饰器的新方法” 替换了原来的方法。
⭐ 为什么要这样调用装饰器?
一句话总结:这是 “在元类中动态给方法加装饰器” 的写法,本质和我们平时用的@show_case_step()
语法糖是等价的,但更灵活(可以在创建类时批量处理方法)。
对比两种给方法加装饰器的方式:
平时手动加装饰器(用语法糖
@
):
如果你在方法定义时手动加装饰器,需要这样写:class HistoryPageBase:@show_case_step() # 语法糖,等价于 查询 = show_case_step()(查询)def 查询(self, data):# 核心逻辑
这种方式需要给每个方法手动加
@show_case_step()
,如果类中有 10 个、100 个方法,会非常繁琐。元类中动态加装饰器(你代码中的写法):
元类通过for
循环遍历类中的所有方法(attr.items()
),对符合条件的方法(比如非特殊方法)自动执行attr[attr_name] = show_case_step()(attr_value)
,批量给所有方法加上装饰器。
不管类中有多少方法,一行代码就能统一处理,既省力又能保证所有方法都遵循同样的规则(比如都记录步骤、都处理异常)。
⭐ 最终效果
当元类创建完类(比如HistoryPageBase
)后,类中所有被处理过的方法(比如 “查询”“点击”)都已经是被show_case_step
装饰后的版本。
当你调用这些方法时(比如history_page.查询("测试数据")
),会自动触发:
- 步骤记录(打印 “执行查询方法,参数是 xxx”);
- 异常处理(如果出错,自动包装成
TrackedException
并记录日志)。
简单说,这句话就是元类的 “自动化工具”—— 批量给类中的方法 “穿上” 装饰器的 “外套”,让它们天生就具备额外的功能,无需人工逐个操作。