doctest 使用方法:笔记|Python 的 doctest 使用方法
"""
Doctest 批量执行器工具类
"""
import doctest
import importlib
import os
from typing import Generator, List, NoReturn
import coverage
class DoctestRunner:
"""Doctest 批量执行器
在构造 DoctestRunner 实例时,需提供项目根目录 source_root;后续逐个使用 add_module 方法添加需要执行
doctest 的模块(自动扫描模块下包含的子模块并添加);添加完成后,执行 run 方法,即批量执行所有添加的模块的 doctest
并整理结果。
如果需要忽略某个模块,则可以使用 add_ignore 方法忽略。
"""
def __init__(self, source_root: str):
"""构造方法
Parameters
----------
source_root : str
项目根目录 (Source Root),后续添加的 module 均需基于项目根目录
"""
# 解析项目路径 (忽略末尾的 "/")
self._source: str = source_root.rstrip("/")
# 所有需要检查的模块
self._modules = set()
# 需要忽略的模块
self._ignore_modules = set()
# 汇总统计结果文件
self._n_failures: int = 0 # 失败用例总数
self._n_tries: int = 0 # 测试用例总数
self._fail_module: List[str] = [] # 失败模块列表
self._error_module: List[str] = [] # 报错模块列表
def add_module(self, module: str, recursion: bool = True) -> NoReturn:
"""添加需要执行 doctest 的 module 模块
Parameters
----------
module : str
需要添加的 module 模块;如果模块下包含子模块,则将自动扫描所有子模块并添加(如不需要添加子模块,则需将参数
recursion 置为 False)
例如:package1.package2.file_name
recursion : bool, default = True
是否递归查找 module 模块下的子模块
"""
self._modules.add(module) # 添加当前 module 模块
if recursion is True:
for sub_module in self._iter_sub_module(module):
self._modules.add(sub_module) # 添加当前 module 下的子模块
def add_ignore(self, module, recursion: bool = True) -> NoReturn:
"""忽略 module 模块不执行 doctest
Parameters
----------
module : str
需要忽略的 module 模块;如果模块下包含子模块,则将自动扫描所有子模块并忽略(如不需要忽略子模块,则需将参数
recursion 置为 False)
例如:package1.package2.file_name
recursion : bool, default = True
是否递归查找 module 模块下的子模块
"""
self._ignore_modules.add(module) # 忽略当前 module 模块
if recursion is True:
for sub_module in self._iter_sub_module(module):
self._ignore_modules.add(sub_module) # 忽略当前 module 下的子模块
def _iter_sub_module(self, module: str) -> Generator[str, None, None]:
"""获取 module 模块下的所有子模块的迭代器"""
path = os.path.join(self._source, *module.split(".")) # 生成 module 所在路径
if os.path.isdir(path):
for root, dirs, files in os.walk(path):
for file in files:
if file.endswith(".py"):
sub_module = root.replace(self._source + "/", "").split("/")
sub_module.append(file.replace(".py", ""))
yield ".".join(sub_module)
def run(self) -> NoReturn:
"""批量执行所有添加的模块的 doctest 并整理结果"""
for module in sorted(self._modules - self._ignore_modules): # 排序以保证相邻路径结果相邻
# noinspection PyBroadException
try:
package = importlib.import_module(module)
failures, tries = doctest.testmod(package, report=False) # 执行 doctest
self._n_failures += failures # 累计报错次数
self._n_tries += tries # 累加尝试次数
if failures > 0:
self._fail_module.append(module)
except Exception:
self._error_module.append(module)
@property
def n_failures(self) -> int:
"""返回失败用例总数"""
return self._n_failures
@property
def n_success(self) -> int:
"""返回成功用例总数"""
return self._n_tries - self._n_failures
@property
def n_tries(self) -> int:
"""返回测试用例总数"""
return self._n_tries
@property
def fail_module(self) -> List[str]:
"""返回失败模块列表"""
return self._fail_module
@property
def n_fail_module(self) -> int:
"""返回失败模块总数"""
return len(self._fail_module)
@property
def error_module(self) -> List[str]:
"""返回报错模块列表"""
return self._error_module
@property
def n_error_module(self) -> int:
"""返回报错模块总数"""
return len(self._error_module)
if __name__ == "__main__":
cov = coverage.coverage()
cov.start()
dr = DoctestRunner("/home/txjiang/power4/src")
dr.add_module("data_govern")
print(dr.run())
cov.stop()
print(cov.html_report(directory="/home/txjiang/doctest"))