目录
这一次,我是要履行在第三篇教程中的承诺,提供一个利用radon分析Python文件并生成报告的小工具。整个小项目我已经压缩发布了,可以直接拿来用。
废话不多说,我们马上开始吧(´・ω・)つt[ ]
前三篇教程加起来一共有16730字,量大管饱。想要学习radon这个库的同志可以参考之:
radon库教程(1)https://blog.csdn.net/2402_85728830/article/details/148185974
radon库教程(2)https://blog.csdn.net/2402_85728830/article/details/148194117
radon库教程(3)https://blog.csdn.net/2402_85728830/article/details/148199802
一. 功能展示
此工具采用Shell+Python编写,内容如下:
首先,将要检查的Python文件放入tests文件夹内:
这里我放入了36个文件。然后运行main.sh,这个脚本是并发执行的,因此速度很快。我等了4.79秒就分析完了。
现在打开reports文件夹,里面全都是新鲜出炉的代码分析报告:
这些报告分为三大部分,每一部分由50个“*”隔开。内容如下:
第一部分是关于可维护性指数的内容。第二部分是关于原始数据的内容。第三部分则是关于模块内函数,方法和类的圈复杂度的内容。对于方法们,报告中会指出它们的所属类:
我还提供了两个Python文件,运行它们可以快速清空tests和reports这两个文件夹。原理是直接删除,所以被删除的文件是不能被拿回来的。
二. 原理简述
下面来简单解释一下这个工具的原理
那两个清空文件夹的就不说了。首先,这个frozenjson.py是一个利用元编程技巧助力分析json文件的工具,在report_maker.py中被导入使用。main.sh是Shell脚本,它的内容如下:
#!/bin/bash
for file_name in ./tests/*.py; do
base_name=$(basename "$file_name") # 获取文件名
base_name_no_ext="${base_name%.*}" # 去掉扩展名
# 并发执行报告生成
(
python -m radon cc "$file_name" -s -j -O "reports/${base_name_no_ext}_cc.json"
python -m radon mi "$file_name" -s -j -O "reports/${base_name_no_ext}_mi.json"
python -m radon raw "$file_name" -s -j -O "reports/${base_name_no_ext}_raw.json"
python report_maker.py "${base_name_no_ext}"
) &
done
# 等待所有后台进程完成
wait
首先,它遍历tests目录下所有后缀是.py的文件,分别获取它们的文件名和去掉扩展名的文件名:
#!/bin/bash
for file_name in ./tests/*.py; do
base_name=$(basename "$file_name") # 获取文件名
base_name_no_ext="${base_name%.*}" # 去掉扩展名
然后,利用这些名字驱动Python的radon库,在reports目录中生成三项指标的json数据文件:
python -m radon cc "$file_name" -s -j -O "reports/${base_name_no_ext}_cc.json"
python -m radon mi "$file_name" -s -j -O "reports/${base_name_no_ext}_mi.json"
python -m radon raw "$file_name" -s -j -O "reports/${base_name_no_ext}_raw.json"
最后,将base_name_no_ext传给report_maker.py脚本,驱动它来使用三项指标数据生成报告:
python report_maker.py "${base_name_no_ext}"
而这个report_maker.py的作用,则是通过接收到的文件名,自动去reports目录中寻找那三个相关的json数据,用它们来生成报告,并删除这些json文件。原理还是比较简单的。
最后,放一下完整代码清单吧。
三. 完整代码清单
文件的结构如下:
main.sh:
#!/bin/bash
for file_name in ./tests/*.py; do
base_name=$(basename "$file_name") # 获取文件名
base_name_no_ext="${base_name%.*}" # 去掉扩展名
# 并发执行报告生成
(
python -m radon cc "$file_name" -s -j -O "reports/${base_name_no_ext}_cc.json"
python -m radon mi "$file_name" -s -j -O "reports/${base_name_no_ext}_mi.json"
python -m radon raw "$file_name" -s -j -O "reports/${base_name_no_ext}_raw.json"
python report_maker.py "${base_name_no_ext}"
) &
done
# 等待所有后台进程完成
wait
clear_the_reports.py:
from pathlib import Path
import shutil
def clear_folder(folder_path):
folder = Path(folder_path)
for item in folder.iterdir():
if item.is_file() or item.is_symlink():
item.unlink() # 删除文件或符号链接
elif item.is_dir():
shutil.rmtree(item) # 递归删除子文件夹
clear_folder("reports")
clear_the_tests.py:
from pathlib import Path
import shutil
def clear_folder(folder_path):
folder = Path(folder_path)
for item in folder.iterdir():
if item.is_file() or item.is_symlink():
item.unlink() # 删除文件或符号链接
elif item.is_dir():
shutil.rmtree(item) # 递归删除子文件夹
clear_folder("tests")
frozenjson.py:
'''
包含一个用于动态解析JSON数据的类。
'''
from __future__ import annotations
from keyword import iskeyword
from collections.abc import Mapping, MutableSequence
from typing import Any, Iterator
class FrozenJSON:
"""
一个表示冻结JSON对象的类。\n
它是不可变的,可以像字典或对象一样访问。\n
用来解析JSON数据。\n
参数:
---
mapping: Mapping\n
要转换为FrozenJSON对象的映射(字典或类似字典的对象)。
"""
__slots__ = ("__data",)
def __init__(self, mapping: Mapping) -> None:
self.__data = {}
for key, value in mapping.items():
if iskeyword(key):
key += "_"
self.__data[key] = value
@property
def data(self) -> Mapping:
return self.__data
def __dir__(self) -> list[str]:
return list(self.__data.keys())
def __getattr__(self, name: str) -> FrozenJSON|list|Any:
try:
return FrozenJSON.build(self.__data[name])
except KeyError:
raise AttributeError(f"'{self.__class__.__name__}' 没有属性 '{name}'")
@classmethod
def build(cls, obj: Any) -> FrozenJSON|list|Any:
if isinstance(obj, Mapping):
return cls(obj)
elif isinstance(obj, MutableSequence):
return [cls.build(item) for item in obj]
else:
return obj
def keys(self) -> Iterator[str]:
for key in self.__data:
yield key
def values(self) -> Iterator[FrozenJSON|list|Any]:
for key in self.__data:
yield self.__getattr__(key)
def items(self) -> Iterator[tuple[str, FrozenJSON|list|Any]]:
for key in self.__data:
yield key, self.__getattr__(key)
def __contains__(self, key: str) -> bool:
return key in self.__data
def __repr__(self) -> str:
return f"{self.__class__.__name__}({self.__data})"
def __iter__(self) -> Iterator[str]:
return iter(self.__data)
def __len__(self) -> int:
return len(self.__data)
def __getitem__(self, key: str) -> FrozenJSON|list|Any:
try:
return FrozenJSON.build(self.__data[key])
except KeyError:
raise KeyError(f"'{self.__class__.__name__}' 没有键 '{key}'")
def __setitem__(self, key: str, value: Any) -> None:
raise AttributeError(f"'{self.__class__.__name__}' 是不可变的")
report_maker.py:
from pathlib import Path
import json
import sys
from frozenjson import FrozenJSON
reports = []
# 文件名从Shell脚本获取
file_name = sys.argv[1]
# 改名用的映射
mi_dict = {
'mi': '可维护性指数',
'rank': '可维护性等级'
}
raw_dict = {
'loc': '总行数',
'lloc': '逻辑行数',
'sloc': '物理行数',
'comments': '单行注释数',
'multi': '多行注释数',
'blank': '空行数'
}
# 文件的路径
cc_path = Path(f'reports/{file_name}_cc.json')
mi_path = Path(f'reports/{file_name}_mi.json')
raw_path = Path(f'reports/{file_name}_raw.json')
# 转换格式为FrozenJson
# 动态获取属性,方便处理
cc = FrozenJSON(json.loads(cc_path.read_text()))
mi = FrozenJSON(json.loads(mi_path.read_text()))
raw = FrozenJSON(json.loads(raw_path.read_text()))
# 第一步:分析模块可维护性数据
reports.append(f'模块名:{file_name}.py')
module_mi = getattr(mi, f'./tests/{file_name}.py')
for attr, value in module_mi.items():
reports.append(f'{mi_dict[attr]}:{value}')
## 分隔符
reports.append('*'*50)
# 第二步:分析原始数据
module_raw = getattr(raw, f'./tests/{file_name}.py')
saves = ('loc', 'sloc', 'comments', 'multi')
saved_raws = {}
for attr, value in module_raw.items():
if attr in raw_dict:
reports.append(f'{raw_dict[attr]}:{value}')
if attr in saves:
saved_raws[attr] = value
saved_raws = FrozenJSON(saved_raws)
## 这里计算占比
reports.append(f'单行注释/总代码行:{format(saved_raws.comments/saved_raws.loc, '.2%')}')
reports.append(f'单行注释/物理行数:{format(saved_raws.comments/saved_raws.sloc, '.2%')}')
reports.append(f'总注释/总代码行:{format((saved_raws.comments+saved_raws.multi)/saved_raws.loc, '.2%')}')
## 分隔符
reports.append('*'*50)
# 第三步:分析圈复杂度数据
reports.append('\t函数,类,方法的圈复杂度报告:')
try:
module_cc = getattr(cc, f'./tests/{file_name}.py') #这是个列表
except AttributeError:
reports.append('文件中没有圈复杂度数据')
else:
for obj in module_cc: #每个obj是FrozenJson对象
belongs = '' if obj.type != 'method' else f'\n所属类:{obj.classname}'
report = f"名称:{obj.name};类型:{obj.type};{belongs}\n圈复杂度:{obj.complexity};圈复杂度等级:{obj.rank}\n" + '-'*50
reports.append(report)
# 写入报告文件
with open(f'reports/{file_name}_report.txt', mode='w', encoding='utf-8') as f:
f.write('\n'.join(reports))
# 删除json文件
for i in ('cc', 'mi', 'raw'):
path = Path(f'reports/{file_name}_{i}.json')
if path.exists():
path.unlink()
希望各位同志在未来的日子里继续学到更多新知识,写出让自己和他人都满意的代码,在编程的事业中收获无穷的乐趣和满足。
(大小姐连着开了四篇的户,这回终于上镜了,我真的哭死)