当前位置: 首页 > news >正文

[日常学习] -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是所有类的 “元类”(比如intstr、你自己写的类,本质上都是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可能是 “最基础的元素类”(直接用这个元类创建),而其他类(比如PortalPageHistoryPageBase)都是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)methodfunction:只处理 “方法 / 函数”(不处理普通属性,比如elementlocator)。
    • 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(添加了elementparent属性,包装了方法),正式创建并返回这个新类。

总结:这个元类到底干了啥?

简单说,所有用ElementMeta创建的类(你的页面类都是它的 “后代”)都会:

  1. 自动带上element(元素实例)和parent(父级对象)两个基础属性,统一管理元素关系。
  2. 除了最基础的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:框架自定义的异常类,比普通异常多了 “上下文信息” 和 “原始异常”,方便调试(比如能看到 “哪个函数在什么参数下出错了”)。
    • loggerwrite_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并记录日志)。

简单说,这句话就是元类的 “自动化工具”—— 批量给类中的方法 “穿上” 装饰器的 “外套”,让它们天生就具备额外的功能,无需人工逐个操作。

http://www.lryc.cn/news/625308.html

相关文章:

  • VSCode 从安装到精通:下载安装与快捷键全指南
  • LINUX 软件编程 -- 线程
  • WebPack》》Loader原理、分类
  • 如何在 Ubuntu Linux 上安装 RPM 软件包
  • 字符分类函数与字符转换函数
  • 在Qt中使用PaddleOCR进行文本识别
  • ubuntu24.04 用apt安装的mysql修改存储路径(文件夹、目录)
  • Vue2+Vue3前端开发_Day1
  • 当宠物机器人装上「第六感」:Deepoc 具身智能如何重构宠物机器人照看逻辑
  • Ubuntu22.04安装docker最新教程,包含安装自动脚本
  • 雷卯针对香橙派Orange Pi 3 LTS开发板防雷防静电方案
  • 在 Windows 上使用 Kind 创建本地 Kubernetes 集群并集成Traefik 进行负载均衡
  • Linux下Nginx安装及负载均衡配置
  • pytest高级用法之插件开发
  • Docker核心---数据卷(堵门秘籍)
  • RxJava 在 Android 即时通讯中的应用:封装、处理与控制
  • OpenHarmony之打造全场景智联基座的“分布式星链 ”WLAN子系统
  • (第五篇)spring cloud之Ribbon负载均衡
  • C语言实战:从零开始编写一个通用配置文件解析器
  • 常见的 Bash 命令及简单脚本
  • 量子计算和超级计算机将彻底改变技术
  • 记录Webapi Excel 导出
  • 【qml-4】qml与c++交互(类型多例)
  • 【CPP】一个CPP的Library(libXXXcore)和测试程序XXX_main的Demo
  • kkfileview预览Excel文件去掉左上角的跳转HTM预览、打印按钮
  • Spring Boot 全局异常处理
  • JVM参数优化
  • 《算法导论》第 29 章 - 线性规划
  • Matplotlib数据可视化实战:Matplotlib子图布局与管理入门
  • Day10--滑动窗口与双指针--2875. 无限数组的最短子数组,76. 最小覆盖子串,632. 最小区间