圈复杂度和radon库(4):生成代码分析报告的小工具

目录

一. 功能展示

二. 原理简述

三. 完整代码清单


这一次,我是要履行在第三篇教程中的承诺,提供一个利用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()

希望各位同志在未来的日子里继续学到更多新知识,写出让自己和他人都满意的代码,在编程的事业中收获无穷的乐趣和满足。

 (大小姐连着开了四篇的户,这回终于上镜了,我真的哭死)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值