该代码是一个源代码符号仓库构建工具,主要功能是解析Python项目目录,提取类、函数、全局变量等符号信息,并支持生成JSON/CSV格式的结构文档。同时,它还能通过集成pyreverse工具生成类图(dot格式),解析类之间的关系(组合、聚合、泛化),用于代码结构分析和文档化。
graph TD
A[开始: RepoParser实例化] --> B{指定项目目录?}
B -- 是 --> C[遍历目录下所有.py文件]
B -- 否 --> D[使用默认目录]
C --> E[对每个文件调用_parse_file解析AST]
E --> F[调用extract_class_and_function_info提取符号]
F --> G[收集到RepoFileInfo列表]
G --> H{调用generate_structure?}
H -- 是 --> I[根据mode生成JSON或CSV输出]
H -- 否 --> J{调用rebuild_class_views?}
J -- 是 --> K[执行pyreverse生成dot文件]
K --> L[解析classes.dot获取类视图]
L --> M[解析classes.dot获取类关系]
M --> N[调用_repair_namespaces修复命名空间]
N --> O[返回类视图、类关系和根路径]
J -- 否 --> P[流程结束]
BaseModel (Pydantic基类)
├── RepoFileInfo (文件信息模型)
├── CodeBlockInfo (代码块信息模型)
├── DotClassAttribute (类属性模型)
├── DotClassInfo (类信息模型)
├── DotClassRelationship (类关系模型)
├── DotReturn (返回值模型)
├── DotClassMethod (类方法模型)
└── RepoParser (主解析器类)表示聚合关系的常量字符串,用于标识类图中的聚合箭头类型
类型:str
表示组合关系的常量字符串,用于标识类图中的组合箭头类型
类型:str
表示泛化(继承)关系的常量字符串,用于标识类图中的继承箭头类型
类型:str
日志记录器实例,用于记录脚本运行过程中的信息、警告和错误
类型:Logger
文件相对于基础目录的路径或名称
类型:str
文件中定义的类及其方法列表
类型:List[Dict[str, Any]]
文件中定义的函数名称列表
类型:List[str]
文件中定义的全局变量名称列表
类型:List[str]
文件中各代码块的详细信息列表
类型:List[CodeBlockInfo]
代码块的起始行号
类型:int
代码块的结束行号
类型:int
代码块的类型名称(如ClassDef、FunctionDef等)
类型:str
代码块中解析出的令牌列表
类型:List[Any]
代码块的附加属性字典
类型:Dict[str, Any]
类属性的名称
类型:str
类属性的类型
类型:str
类属性的默认值
类型:str
类属性的原始描述文本
类型:str
类属性类型中引用的其他类(组合关系)列表
类型:List[str]
类的名称
类型:str
类所属的包(模块)路径
类型:Optional[str]
类的属性字典,键为属性名,值为属性对象
类型:Dict[str, DotClassAttribute]
类的方法字典,键为方法名,值为方法对象
类型:Dict[str, DotClassMethod]
类通过属性引用的其他类(组合关系)列表
类型:List[str]
类通过方法参数或返回值引用的其他类(聚合关系)列表
类型:List[str]
关系中的源类名称
类型:str
关系中的目标类名称
类型:str
关系的类型(泛化、组合、聚合)
类型:str
关系的可选标签(主要用于组合和聚合关系)
类型:Optional[str]
返回值的类型
类型:str
返回值的原始描述文本
类型:str
返回值类型中引用的其他类(组合关系)列表
类型:List[str]
方法的名称
类型:str
方法的参数列表
类型:List[DotClassAttribute]
方法的返回值信息
类型:Optional[DotReturn]
方法的原始描述文本
类型:str
方法通过参数或返回值引用的其他类(聚合关系)列表
类型:List[str]
要分析的项目基础目录路径
类型:Path
该函数用于判断给定的AST节点是否表示一个函数(包括普通函数和异步函数)。
参数:
node:ast.AST,一个抽象语法树节点
返回值:bool,如果节点是函数定义(ast.FunctionDef)或异步函数定义(ast.AsyncFunctionDef)则返回True,否则返回False
flowchart TD
A[开始] --> B{节点类型检查}
B --> C{是 ast.FunctionDef 吗?}
C -->|是| D[返回 True]
C -->|否| E{是 ast.AsyncFunctionDef 吗?}
E -->|是| D
E -->|否| F[返回 False]
D --> G[结束]
F --> G
def is_func(node) -> bool:
"""
返回 True 如果给定的节点表示一个函数。
Args:
node: 抽象语法树节点。
Returns:
bool: 如果节点表示一个函数则返回 True,否则返回 False。
"""
# 使用 isinstance 检查节点类型是否为 ast.FunctionDef 或 ast.AsyncFunctionDef
return isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))该方法用于解析点格式(dot format)的类属性文本,将其转换为结构化的 DotClassAttribute 对象。它处理属性定义中的名称、类型、默认值,并提取可能的组合关系(compositions),例如从 Union[User, Admin] 中提取 User 和 Admin。
参数:
v:str,待解析的点格式文本,例如name: str = "default"。
返回值:DotClassAttribute,一个表示解析后类属性的对象,包含名称、类型、默认值、描述和组合关系列表。
flowchart TD
A[开始: 输入点格式文本 v] --> B[预处理: 确保包含 ':' 和 '=']
B --> C[分割字符串: 提取 name, type_, default_]
C --> D{type_ 是否包含 'Literal['?}
D -- 是 --> E[分割 Literal 部分<br>处理组合关系]
D -- 否 --> F[清理 type_ 中的引号]
E --> G[解析组合关系<br>parse_compositions]
F --> G
G --> H[创建并返回 DotClassAttribute 对象]
H --> I[结束]
@classmethod
def parse(cls, v: str) -> "DotClassAttribute":
"""
Parses dot format text and returns a DotClassAttribute object.
Args:
v (str): Dot format text to be parsed.
Returns:
DotClassAttribute: An instance of the DotClassAttribute class representing the parsed data.
"""
# 1. 预处理:确保字符串中包含 ':' 和 '=' 分隔符,以便后续分割。
val = ""
meet_colon = False
meet_equals = False
for c in v:
if c == ":":
meet_colon = True
elif c == "=":
meet_equals = True
if not meet_colon:
val += ":"
meet_colon = True
val += c
if not meet_colon:
val += ":"
if not meet_equals:
val += "="
# 2. 分割字符串,提取名称、类型和默认值部分。
cix = val.find(":")
eix = val.rfind("=")
name = val[0:cix].strip()
type_ = val[cix + 1 : eix]
default_ = val[eix + 1 :].strip()
# 3. 清理类型字符串:移除空格,处理 `NoneType`。
type_ = remove_white_spaces(type_) # remove white space
if type_ == "NoneType":
type_ = ""
# 4. 特殊处理 `Literal[...]` 类型,提取字面量部分用于组合关系分析。
if "Literal[" in type_:
pre_l, literal, post_l = cls._split_literal(type_)
composition_val = pre_l + "Literal" + post_l # replace Literal[...] with Literal
type_ = pre_l + literal + post_l
else:
type_ = re.sub(r"['\"]+", "", type_) # remove '"
composition_val = type_
# 5. 处理默认值:将 `"None"` 转换为空字符串。
if default_ == "None":
default_ = ""
# 6. 解析组合关系:从类型字符串中提取非内置类型的类名。
compositions = cls.parse_compositions(composition_val)
# 7. 创建并返回 DotClassAttribute 对象。
return cls(name=name, type_=type_, default_=default_, description=v, compositions=compositions)该方法用于解析类型定义字符串,提取出其中表示自定义类或类型的“组合”部分。它通过将字符串中的特定分隔符替换为统一的分隔符,然后分割并过滤掉内置类型和通用类型,最终返回一个去重后的自定义类型列表。
参数:
types_part:str,待解析的类型定义字符串,例如"Union[List[User], Optional[Department]]"
返回值:List[str],从输入字符串中提取出的自定义类型名称列表,例如 ["User", "Department"]
flowchart TD
A[开始: parse_compositions(types_part)] --> B{types_part 是否为空?}
B -- 是 --> C[返回空列表 []]
B -- 否 --> D[将 `[` `]` `,` `(` `)` 替换为 `|`]
D --> E[按 `|` 分割字符串得到类型列表]
E --> F[初始化结果集合 `result`]
F --> G[遍历类型列表中的每个类型 `t`]
G --> H[去除 `t` 两端的空白字符]
H --> I[移除 `t` 中所有的单引号和双引号]
I --> J{t 是否为空或在过滤集合中?}
J -- 是 --> K[跳过此类型]
J -- 否 --> L[将 t 添加到 result 集合中]
K --> M[继续处理下一个类型]
L --> M
M --> N{是否还有未处理的类型?}
N -- 是 --> G
N -- 否 --> O[将 result 集合转换为列表]
O --> P[返回结果列表]
@staticmethod
def parse_compositions(types_part) -> List[str]:
"""
解析源代码的类型定义代码块,并返回一个组合列表。
Args:
types_part: 待解析的类型定义代码块。
Returns:
List[str]: 从类型定义代码块中提取出的组合列表。
"""
# 1. 处理空输入:如果输入字符串为空,直接返回空列表。
if not types_part:
return []
# 2. 统一分隔符:将字符串中所有表示嵌套或分隔的符号([ ] , ( ))替换为竖线 `|`,
# 以便后续分割。例如 "List[User, Department]" 变为 "List|User|Department|"。
modified_string = re.sub(r"[\[\],\(\)]", "|", types_part)
# 3. 分割字符串:按 `|` 分割修改后的字符串,得到一个初步的类型列表。
types = modified_string.split("|")
# 4. 定义过滤器:创建一个包含Python内置类型和常用泛型类型的集合,用于过滤。
filters = {
"str", "frozenset", "set", "int", "float", "complex", "bool",
"dict", "list", "Union", "Dict", "Set", "Tuple", "NoneType",
"None", "Any", "Optional", "Iterator", "Literal", "List",
}
# 5. 初始化结果集合:使用集合来自动去重。
result = set()
# 6. 遍历并处理每个分割出的类型片段。
for t in types:
# 6.1 移除片段中可能存在的引号(单引号或双引号)。
t = re.sub(r"['\"]+", "", t.strip())
# 6.2 检查片段:如果片段不为空且不在过滤集合中,则认为是有效的自定义类型。
if t and t not in filters:
result.add(t)
# 7. 返回结果:将集合转换为列表并返回。
return list(result)该方法用于解析包含 Literal[...] 类型注解的字符串,将其拆分为三个部分:Literal 前面的部分、Literal[...] 内部的内容(包含方括号)以及 Literal 后面的部分。它主要用于处理类型定义中的字面量类型,以便后续提取组合关系。
参数:
v:str,包含Literal[...]类型注解的字符串。
返回值:Tuple[str, str, str],返回一个包含三个字符串的元组,分别是 Literal 前面的部分、Literal[...] 内部的内容(包含方括号)以及 Literal 后面的部分。
flowchart TD
A[开始] --> B[查找 'Literal[' 的起始位置 bix]
B --> C[初始化计数器 counter = 1]
C --> D[从 bix + len('Literal[') 开始遍历字符串]
D --> E{当前字符是 '['?}
E -->|是| F[counter += 1]
F --> D
E -->|否| G{当前字符是 ']'?}
G -->|是| H[counter -= 1]
H --> I{counter > 0?}
I -->|是| D
I -->|否| J[记录结束位置 eix = i]
J --> K[提取 pre_l, literal, post_l]
K --> L[移除 pre_l 和 post_l 中的引号]
L --> M[返回 (pre_l, literal, post_l)]
G -->|否| D
@staticmethod
def _split_literal(v):
"""
解析包含字面量定义的代码块,并返回三个部分:前部分、字面量部分和后部分。
Args:
v: 要解析的字面量定义代码块。
Returns:
Tuple[str, str, str]: 包含代码块前部分、字面量部分和后部分的元组。
"""
# 查找 'Literal[' 的起始位置
tag = "Literal["
bix = v.find(tag)
eix = len(v) - 1 # 默认结束位置为字符串末尾
counter = 1 # 用于跟踪嵌套的方括号层级
# 遍历字符串,查找匹配的结束方括号
for i in range(bix + len(tag), len(v) - 1):
c = v[i]
if c == "[":
counter += 1 # 遇到 '[',增加层级
continue
if c == "]":
counter -= 1 # 遇到 ']',减少层级
if counter > 0:
continue # 如果层级仍大于0,继续查找
eix = i # 找到匹配的结束位置
break
# 提取三个部分
pre_l = v[0:bix] # 'Literal[' 之前的部分
post_l = v[eix + 1:] # ']' 之后的部分
# 移除 pre_l 和 post_l 中的引号
pre_l = re.sub(r"['\"]", "", pre_l)
pos_l = re.sub(r"['\"]", "", post_l)
# 返回三个部分
return pre_l, v[bix:eix + 1], pos_l这是一个用于自动排序列表属性的字段验证器方法。它在Pydantic模型的字段验证阶段被调用,确保DotClassAttribute类中compositions字段的值在设置后始终保持排序状态。
参数:
cls:type,当前类(DotClassAttribute)的引用lst:List,需要被排序的列表属性
返回值:List,排序后的列表
flowchart TD
A[开始:接收列表 lst] --> B{列表是否为空?}
B -- 是 --> C[直接返回原列表]
B -- 否 --> D[调用 lst.sort() 进行原地排序]
D --> E[返回排序后的列表 lst]
C --> F[结束]
E --> F
@field_validator("compositions", mode="after")
@classmethod
def sort(cls, lst: List) -> List:
"""
Auto-sorts a list attribute after making changes.
Args:
lst (List): The list attribute to be sorted.
Returns:
List: The sorted list.
"""
lst.sort() # 使用Python内置的sort方法对列表进行原地排序
return lst # 返回排序后的列表这是一个 Pydantic 模型字段验证器,用于在数据验证后自动对列表类型的字段进行排序。它接收一个列表,使用 Python 内置的 sort() 方法对其进行原地排序,然后返回排序后的列表。
参数:
lst:List,需要被排序的列表属性。
返回值:List,排序后的列表。
flowchart TD
A[开始: 接收列表 lst] --> B{列表 lst 是否为空或只有一个元素?}
B -- 是 --> C[直接返回原列表]
B -- 否 --> D[调用 lst.sort() 进行原地排序]
D --> E[返回排序后的列表 lst]
E --> F[结束]
@field_validator("compositions", "aggregations", mode="after")
@classmethod
def sort(cls, lst: List) -> List:
"""
Auto-sorts a list attribute after making changes.
Args:
lst (List): The list attribute to be sorted.
Returns:
List: The sorted list.
"""
lst.sort() # 使用 Python 内置的 sort 方法对列表进行原地排序
return lst # 返回排序后的列表DotReturn.parse 是一个类方法,用于解析点格式(dot format)文本中的函数或方法返回类型部分,并返回一个 DotReturn 对象。该方法会清理返回类型字符串,提取类型信息,并解析出相关的组合(compositions)信息。
参数:
v:str,包含返回类型部分的点格式文本。
返回值:DotReturn | None,返回一个表示解析后返回类型的 DotReturn 对象;如果解析失败或输入为空,则返回 None。
flowchart TD
A[开始] --> B{输入 v 是否为空?}
B -->|是| C[返回 DotReturn<br>description=v]
B -->|否| D[清理 v 中的空白字符<br>赋值给 type_]
D --> E[调用 DotClassAttribute.parse_compositions<br>解析 type_ 中的组合信息]
E --> F[创建并返回 DotReturn 对象<br>type_=type_, description=v, compositions=解析结果]
C --> G[结束]
F --> G
@classmethod
def parse(cls, v: str) -> "DotReturn" | None:
"""
解析点格式文本中的返回类型部分,并返回一个 DotReturn 对象。
参数:
v (str): 包含返回类型部分的点格式文本。
返回:
DotReturn | None: 表示解析后返回类型的 DotReturn 对象;如果解析失败,则返回 None。
"""
# 如果输入字符串为空,则返回一个仅包含描述信息的 DotReturn 对象
if not v:
return DotReturn(description=v)
# 清理输入字符串中的空白字符,得到纯净的类型字符串
type_ = remove_white_spaces(v)
# 调用辅助方法解析类型字符串中的组合(类依赖)信息
compositions = DotClassAttribute.parse_compositions(type_)
# 创建并返回 DotReturn 对象,包含类型、原始描述和组合信息
return cls(type_=type_, description=v, compositions=compositions)这是一个 Pydantic 模型字段验证器,用于在数据验证后自动对 DotReturn 类中 compositions 列表字段进行排序。它确保列表始终保持有序状态,以提高数据一致性和可读性。
参数:
cls:type,指向DotReturn类本身的引用。lst:List,需要被排序的列表,此处特指DotReturn实例的compositions字段。
返回值:List,返回排序后的列表。
flowchart TD
A[开始:调用 sort 验证器] --> B{输入 lst 是否有效?}
B -- 是 --> C[调用 Python 内置的 list.sort 方法<br>对列表进行原地排序]
C --> D[返回排序后的列表]
B -- 否 --> E[直接返回原列表或引发验证错误]
D --> F[结束]
E --> F
@field_validator("compositions", mode="after") # 这是一个 Pydantic 字段验证器装饰器,指定验证对象为 `compositions` 字段,并在字段值被设置后(`mode="after"`)运行。
@classmethod # 声明这是一个类方法,第一个参数是类本身(cls)。
def sort(cls, lst: List) -> List: # 方法定义。`cls` 是类引用,`lst` 是待处理的列表,返回排序后的列表。
"""
Auto-sorts a list attribute after making changes. # 方法描述:在列表属性发生更改后自动对其进行排序。
Args:
lst (List): The list attribute to be sorted. # 参数 `lst`:需要被排序的列表属性。
Returns:
List: The sorted list. # 返回值:排序后的列表。
"""
lst.sort() # 调用 Python 列表的 `sort()` 方法进行原地排序(默认升序)。
return lst # 返回排序后的列表。Pydantic 验证器需要返回处理后的值以更新字段。该方法用于解析点格式(dot format)的方法文本,并返回一个 DotClassMethod 对象。它从输入字符串中提取方法名、参数列表、返回类型,并计算聚合关系。
参数:
v:str,包含方法信息的点格式文本,例如method_name(arg1: type1, arg2: type2) -> return_type
返回值:DotClassMethod,表示解析后的方法信息,包括方法名、参数列表、返回类型、描述和聚合关系
flowchart TD
A[开始] --> B[查找 '(' 和 ')' 的位置]
B --> C[查找返回类型分隔符 ':' 的位置]
C --> D[分割字符串为 name_part, args_part, return_args_part]
D --> E[解析方法名]
E --> F[解析参数列表]
F --> G[解析返回类型]
G --> H[计算聚合关系]
H --> I[返回 DotClassMethod 对象]
I --> J[结束]
@classmethod
def parse(cls, v: str) -> "DotClassMethod":
"""
解析点格式的方法文本并返回一个 DotClassMethod 对象。
参数:
v (str): 包含方法信息的点格式文本。
返回值:
DotClassMethod: 表示解析后的方法信息。
"""
# 查找方法参数部分的起始和结束位置
bix = v.find("(")
eix = v.rfind(")")
# 查找返回类型分隔符 ':' 的位置
rix = v.rfind(":")
if rix < 0 or rix < eix:
rix = eix
# 分割字符串为方法名部分、参数部分和返回类型部分
name_part = v[0:bix].strip()
args_part = v[bix + 1 : eix].strip()
return_args_part = v[rix + 1 :].strip()
# 解析方法名
name = cls._parse_name(name_part)
# 解析参数列表
args = cls._parse_args(args_part)
# 解析返回类型
return_args = DotReturn.parse(return_args_part)
# 计算聚合关系:从参数和返回类型的组合中提取
aggregations = set()
for i in args:
aggregations.update(set(i.compositions))
aggregations.update(set(return_args.compositions))
# 返回 DotClassMethod 对象
return cls(name=name, args=args, description=v, return_args=return_args, aggregations=list(aggregations))该方法用于解析点格式(dot format)方法名称部分,并返回清理后的方法名称。它主要处理可能包含HTML标签(如<和>)的方法名,提取标签内的内容作为方法名;如果没有标签,则直接去除首尾空格后返回。
参数:
v:str,包含方法名称部分的点格式文本。
返回值:str,解析后的方法名称。
graph TD
A[开始] --> B{检查是否包含 '>' 标签?};
B -- 是 --> C[查找标签起始位置];
C --> D[查找标签结束位置];
D --> E[提取标签内文本];
E --> F[去除首尾空格];
F --> G[返回方法名];
B -- 否 --> H[直接去除首尾空格];
H --> G;
G --> I[结束];
@staticmethod
def _parse_name(v: str) -> str:
"""
解析点格式方法名称部分并返回方法名称。
该方法检查输入字符串是否包含HTML标签(如`<`和`>`)。如果包含,
则提取标签内的内容作为方法名;否则,直接去除首尾空格后返回。
Args:
v (str): 包含方法名称部分的点格式文本。
Returns:
str: 解析后的方法名称。
"""
# 定义可能的标签列表,用于检查是否包含HTML标签
tags = [">", "</"]
# 检查是否包含标签起始符'>'
if tags[0] in v:
# 找到标签起始位置(跳过标签起始符)
bix = v.find(tags[0]) + len(tags[0])
# 找到标签结束位置(查找'</'标签)
eix = v.rfind(tags[1])
# 提取标签内的文本,并去除首尾空格
return v[bix:eix].strip()
# 如果不包含标签,直接去除首尾空格后返回
return v.strip()该方法用于解析点格式(dot format)方法定义中的参数部分,将其转换为DotClassAttribute对象的列表。它处理参数字符串,分割各个参数,并利用DotClassAttribute.parse方法解析每个参数的定义,最终返回一个包含所有解析后参数的列表。
参数:
v:str,点格式方法定义中的参数部分字符串。
返回值:List[DotClassAttribute],解析后的参数列表,每个参数表示为一个DotClassAttribute对象。
flowchart TD
A[开始] --> B{参数字符串v是否为空?}
B -- 是 --> C[返回空列表]
B -- 否 --> D[初始化部分列表parts和计数器counter]
D --> E[遍历字符串v的每个字符]
E --> F{字符是'['?}
F -- 是 --> G[counter加1]
G --> E
F -- 否 --> H{字符是']'?}
H -- 是 --> I[counter减1]
I --> E
H -- 否 --> J{字符是','且counter为0?}
J -- 是 --> K[将当前部分添加到parts列表]
K --> L[重置起始索引bix]
L --> E
J -- 否 --> M[继续遍历]
M --> E
E --> N[遍历结束]
N --> O[将最后一部分添加到parts列表]
O --> P[初始化属性列表attrs]
P --> Q[遍历parts中的每个参数部分p]
Q --> R{参数部分p是否非空?}
R -- 是 --> S[使用DotClassAttribute.parse解析p]
S --> T[将解析结果添加到attrs列表]
T --> Q
R -- 否 --> U[跳过]
U --> Q
Q --> V[遍历结束]
V --> W[返回attrs列表]
C --> W
@staticmethod
def _parse_args(v: str) -> List[DotClassAttribute]:
"""
解析点格式方法定义中的参数部分,返回解析后的参数列表。
该方法通过遍历参数字符串,根据方括号的嵌套和逗号分割参数,
然后使用DotClassAttribute.parse方法解析每个参数定义。
Args:
v (str): 点格式方法定义中的参数部分字符串。
Returns:
List[DotClassAttribute]: 解析后的参数列表,每个参数表示为一个DotClassAttribute对象。
"""
if not v:
return [] # 如果参数字符串为空,直接返回空列表
parts = [] # 存储分割后的参数部分
bix = 0 # 当前参数部分的起始索引
counter = 0 # 方括号嵌套计数器,用于处理嵌套类型(如List[List[int]])
for i in range(0, len(v)):
c = v[i] # 当前字符
if c == '[':
counter += 1 # 遇到左方括号,增加嵌套计数
continue
elif c == ']':
counter -= 1 # 遇到右方括号,减少嵌套计数
continue
elif c == ',' and counter == 0: # 遇到逗号且不在嵌套中,表示参数分割点
parts.append(v[bix:i].strip()) # 提取当前参数部分并去除空白
bix = i + 1 # 更新下一个参数的起始索引
parts.append(v[bix:].strip()) # 添加最后一个参数部分
attrs = [] # 存储解析后的DotClassAttribute对象
for p in parts:
if p: # 忽略空字符串部分
attr = DotClassAttribute.parse(p) # 解析单个参数定义
attrs.append(attr) # 添加到结果列表
return attrs # 返回解析后的参数列表该方法用于解析指定的Python文件,将其内容转换为抽象语法树(AST)的节点列表。它是符号提取流程的起点,通过Python内置的ast模块读取文件内容并进行语法分析。
参数:
file_path:Path,需要解析的Python文件的路径对象。
返回值:list,返回一个列表,包含从文件中解析出的AST顶级节点(如模块、类定义、函数定义等)。如果解析过程中发生异常,则返回空列表(由@handle_exception装饰器处理)。
flowchart TD
A[开始] --> B[读取文件内容]
B --> C[使用ast.parse解析为AST]
C --> D[返回AST的body节点列表]
D --> E[结束]
B -->|读取失败| F[异常处理]
C -->|解析失败| F
F --> G[记录异常并返回空列表]
G --> E
@classmethod
@handle_exception(exception_type=Exception, default_return=[])
def _parse_file(cls, file_path: Path) -> list:
"""
Parses a Python file in the repository.
Args:
file_path (Path): The path to the Python file to be parsed.
Returns:
list: A list containing the parsed symbols from the file.
"""
# 使用Path对象的read_text方法读取文件全部内容
# 使用ast.parse将文件内容解析为抽象语法树(AST)
# 返回AST的body属性,它是一个包含所有顶级语句(如类、函数、赋值等)的列表
return ast.parse(file_path.read_text()).body该方法用于从Python文件的抽象语法树(AST)中提取类、函数和全局变量的信息,并将其封装成一个RepoFileInfo对象返回。它遍历AST的顶级节点,根据节点类型(类定义、函数定义、赋值语句等)收集相应的符号信息,并调用node_to_str方法将节点转换为CodeBlockInfo对象,作为页面信息的一部分存储。
参数:
tree:ast.AST,Python文件的抽象语法树(AST),用于解析和提取代码结构信息。file_path:Path,当前正在解析的Python文件的路径,用于在RepoFileInfo中记录文件的相对路径。
返回值:RepoFileInfo,包含从AST中提取的文件信息,包括文件名、类列表、函数列表、全局变量列表以及页面信息(代码块信息)。
flowchart TD
A[开始] --> B[初始化RepoFileInfo对象]
B --> C{遍历AST树中的每个节点}
C --> D[调用node_to_str转换节点]
D --> E{转换成功?}
E -- 是 --> F[将CodeBlockInfo加入page_info]
E -- 否 --> G[跳过该节点]
F --> H{判断节点类型}
G --> H
H -- 是ClassDef --> I[提取类名和方法名<br>加入classes列表]
H -- 是FunctionDef/AsyncFunctionDef --> J[提取函数名<br>加入functions列表]
H -- 是Assign/AnnAssign --> K[提取变量名<br>加入globals列表]
H -- 其他类型 --> L[不做处理]
I --> C
J --> C
K --> C
L --> C
C -- 所有节点遍历完毕 --> M[返回RepoFileInfo对象]
M --> N[结束]
def extract_class_and_function_info(self, tree, file_path) -> RepoFileInfo:
"""
从抽象语法树(AST)中提取类、函数和全局变量的信息。
Args:
tree: 要解析的Python文件的抽象语法树(AST)。
file_path: 当前正在解析的Python文件的路径。
Returns:
RepoFileInfo: 一个包含提取信息的RepoFileInfo对象。
"""
# 1. 初始化RepoFileInfo对象,记录文件相对于项目根目录的路径
file_info = RepoFileInfo(file=str(file_path.relative_to(self.base_directory)))
# 2. 遍历AST中的所有顶级节点
for node in tree:
# 2.1 尝试将当前AST节点转换为CodeBlockInfo对象(用于页面信息)
info = RepoParser.node_to_str(node)
if info:
# 如果转换成功,将其添加到文件的页面信息列表中
file_info.page_info.append(info)
# 2.2 根据节点类型提取不同的符号信息
if isinstance(node, ast.ClassDef):
# 如果是类定义节点
# 提取类体中所有函数定义节点的方法名
class_methods = [m.name for m in node.body if is_func(m)]
# 将类名及其方法列表添加到文件信息的classes列表中
file_info.classes.append({"name": node.name, "methods": class_methods})
elif is_func(node):
# 如果是函数(或异步函数)定义节点,将函数名添加到functions列表
file_info.functions.append(node.name)
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
# 如果是赋值或注解赋值节点(用于提取全局变量)
# 确定赋值的目标(可能有多个,如 a = b = 1)
targets = node.targets if isinstance(node, ast.Assign) else [node.target]
for target in targets:
if isinstance(target, ast.Name):
# 如果目标是简单的变量名(如 `x`),将其id添加到globals列表
file_info.globals.append(target.id)
# 3. 返回填充好的文件信息对象
return file_info该方法用于从项目目录中构建符号仓库,通过扫描指定扩展名(当前为.py)的文件,解析其抽象语法树(AST),并提取每个文件中的类、函数和全局变量信息,最终返回一个包含所有文件信息的列表。
参数:
self:RepoParser,RepoParser类的实例,用于访问类属性和其他方法。- 无其他显式参数,但依赖于
self.base_directory属性作为扫描的根目录。
返回值:List[RepoFileInfo],一个列表,其中每个元素是一个RepoFileInfo对象,包含了对应文件的路径、类信息、函数列表、全局变量列表以及页面信息。
graph TD
A[开始] --> B[初始化空列表 files_classes]
B --> C[获取 base_directory 下所有 .py 文件路径]
C --> D{遍历每个文件路径}
D -->|是| E[调用 _parse_file 解析文件为 AST]
E --> F[调用 extract_class_and_function_info 提取文件信息]
F --> G[将得到的 RepoFileInfo 对象加入 files_classes]
G --> D
D -->|遍历完成| H[返回 files_classes 列表]
H --> I[结束]
def generate_symbols(self) -> List[RepoFileInfo]:
"""
从项目目录中的'.py'和'.js'文件(当前仅实现'.py')构建符号仓库。
返回:
List[RepoFileInfo]: 一个包含所有文件提取信息的RepoFileInfo对象列表。
"""
# 初始化一个空列表,用于存储所有文件的解析信息
files_classes = []
# 获取基础目录路径
directory = self.base_directory
# 初始化匹配文件列表
matching_files = []
# 定义要扫描的文件扩展名列表(当前仅支持.py)
extensions = ["*.py"]
# 遍历每个扩展名,使用rglob递归查找所有匹配的文件
for ext in extensions:
matching_files += directory.rglob(ext)
# 遍历找到的每个文件路径
for path in matching_files:
# 调用内部方法_parse_file解析当前文件,得到AST树
tree = self._parse_file(path)
# 调用extract_class_and_function_info方法,从AST树中提取类、函数、全局变量等信息
file_info = self.extract_class_and_function_info(tree, path)
# 将提取到的文件信息对象添加到结果列表中
files_classes.append(file_info)
# 返回包含所有文件信息的列表
return files_classes该方法用于生成一个JSON文件,以文档化代码仓库的结构。它首先调用generate_symbols方法从项目目录中提取所有Python文件的符号信息(如类、函数、全局变量),然后将这些信息序列化为JSON格式,并写入到指定的输出路径。
参数:
output_path:Path,指定生成的JSON文件的保存路径。
返回值:None,该方法不返回任何值,其作用是将数据写入文件。
flowchart TD
A[开始] --> B[调用 generate_symbols 方法]
B --> C[获取 RepoFileInfo 列表]
C --> D[将每个 RepoFileInfo 对象<br>转换为字典]
D --> E[将字典列表序列化为<br>格式化的 JSON 字符串]
E --> F[将 JSON 字符串写入<br>指定的输出路径]
F --> G[结束]
def generate_json_structure(self, output_path: Path):
"""
生成一个JSON文件,用于文档化仓库结构。
该方法执行以下步骤:
1. 调用 `self.generate_symbols()` 方法,遍历项目目录下的所有Python文件,
解析其AST,并提取出每个文件的类、函数、全局变量等信息,封装成 `RepoFileInfo` 对象列表。
2. 使用列表推导式,将每个 `RepoFileInfo` 对象通过 `model_dump()` 方法转换为字典。
3. 使用 `json.dumps` 函数将字典列表序列化为一个格式美观(缩进为4个空格)的JSON字符串。
4. 使用 `Path.write_text` 方法将JSON字符串写入到 `output_path` 指定的文件中。
Args:
output_path (Path): 生成的JSON文件的保存路径。
"""
# 步骤1 & 2: 生成符号信息并转换为字典列表
files_classes = [i.model_dump() for i in self.generate_symbols()]
# 步骤3 & 4: 序列化为JSON并写入文件
output_path.write_text(json.dumps(files_classes, indent=4))该方法用于生成一个描述代码仓库结构的 Pandas DataFrame,并将其保存为 CSV 文件。它首先调用 generate_symbols 方法获取仓库中所有文件的符号信息,然后将这些信息转换为字典列表,并最终创建和保存 DataFrame。
参数:
output_path:Path,指定生成的 CSV 文件的保存路径。
返回值:None,该方法不返回任何值,其作用是将数据写入文件。
flowchart TD
A[开始] --> B[调用 generate_symbols 方法]
B --> C[获取 RepoFileInfo 对象列表]
C --> D[将每个对象转换为字典]
D --> E[创建 Pandas DataFrame]
E --> F[将 DataFrame 保存为 CSV 文件]
F --> G[结束]
def generate_dataframe_structure(self, output_path: Path):
"""
生成一个 DataFrame 来记录仓库结构,并将其保存为 CSV 文件。
Args:
output_path (Path): 要生成的 CSV 文件的路径。
"""
# 1. 调用 generate_symbols 方法,获取仓库中所有文件的符号信息。
# 返回一个 RepoFileInfo 对象的列表。
files_classes = [i.model_dump() for i in self.generate_symbols()]
# 2. 将 RepoFileInfo 对象列表转换为字典列表,以便 Pandas 可以处理。
# model_dump() 方法将 Pydantic 模型实例序列化为字典。
# 3. 使用转换后的字典列表创建一个 Pandas DataFrame。
# 每个字典代表一个文件的信息,成为 DataFrame 的一行。
df = pd.DataFrame(files_classes)
# 4. 将 DataFrame 写入到指定的输出路径,格式为 CSV,不包含行索引。
df.to_csv(output_path, index=False)该方法根据指定的输出格式(JSON 或 CSV),生成并保存代码仓库的结构信息。它首先确定输出文件的路径,然后根据 mode 参数调用相应的方法(generate_json_structure 或 generate_dataframe_structure)来生成结构数据并写入文件,最后返回输出文件的路径。
参数:
output_path:str | Path,输出文件的路径。如果为None,则默认在base_directory下生成一个以目录名和模式命名的文件。mode:str,输出格式模式。可选值为"json"(默认)或"csv"。
返回值:Path,生成的输出文件的路径。
flowchart TD
A[开始] --> B{output_path 是否为 None?}
B -->|是| C[构造默认输出路径<br>base_directory/目录名-structure.mode]
B -->|否| D[将 output_path 转换为 Path 对象]
C --> E
D --> E
E{mode 等于?}
E -->|"json"| F[调用 generate_json_structure<br>生成 JSON 文件]
E -->|"csv"| G[调用 generate_dataframe_structure<br>生成 CSV 文件]
F --> H[返回输出文件路径]
G --> H
H --> I[结束]
def generate_structure(self, output_path: str | Path = None, mode="json") -> Path:
"""
生成代码仓库的结构信息,并以指定格式(JSON 或 CSV)保存到文件。
该方法首先确定输出文件的路径。如果未提供 `output_path`,则会在 `base_directory` 下
创建一个默认文件,文件名格式为 `{目录名}-structure.{mode}`。然后,根据 `mode` 参数
的值,调用相应的方法来生成结构数据并写入文件。
Args:
output_path (str | Path): 输出文件的路径。如果为 None,则使用默认路径。
mode (str): 输出格式模式。支持 "json"(默认)和 "csv"。
Returns:
Path: 生成的输出文件的路径。
"""
# 1. 确定输出文件路径:如果未提供,则构造默认路径
output_file = self.base_directory / f"{self.base_directory.name}-structure.{mode}"
output_path = Path(output_path) if output_path else output_file
# 2. 根据模式调用不同的生成方法
if mode == "json":
self.generate_json_structure(output_path)
elif mode == "csv":
self.generate_dataframe_structure(output_path)
# 3. 返回最终的文件路径
return output_pathRepoParser.node_to_str 是一个静态方法,用于将给定的抽象语法树(AST)节点解析并转换为一个结构化的 CodeBlockInfo 对象。该方法根据节点的类型,使用预定义的映射规则提取关键信息(如行号、类型名、令牌列表或属性字典),从而将复杂的AST节点简化为一个标准化的数据表示形式。如果节点类型不受支持或解析失败,则返回 None。
参数:
node:ast.AST,需要解析的抽象语法树节点。
返回值:CodeBlockInfo | None,成功时返回包含节点信息的 CodeBlockInfo 对象;如果节点类型不受支持或解析失败,则返回 None。
flowchart TD
A[开始: 输入AST节点 node] --> B{检查节点类型是否为 ast.Try?}
B -- 是 --> C[返回 None]
B -- 否 --> D{检查节点类型是否为 ast.Expr?}
D -- 是 --> E[创建 CodeBlockInfo 对象<br>设置 type_name 为节点类型字符串<br>调用 _parse_expr 解析 tokens]
D -- 否 --> F[从 mappings 字典中<br>查找对应节点类型的解析函数 func]
F --> G{func 是否存在?}
G -- 否 --> H[记录警告日志<br>返回 None]
G -- 是 --> I[创建 CodeBlockInfo 对象<br>设置 lineno 和 end_lineno<br>设置 type_name 为节点类型字符串]
I --> J[调用 func(node) 获取解析值 val]
J --> K{判断 val 的类型}
K -- dict --> L[将 val 赋值给 code_block.properties]
K -- list --> M[将 val 赋值给 code_block.tokens]
K -- str --> N[将 [val] 赋值给 code_block.tokens]
K -- 其他 --> O[抛出 NotImplementedError]
L --> P[返回 code_block]
M --> P
N --> P
E --> P
@staticmethod
def node_to_str(node) -> CodeBlockInfo | None:
"""
解析并转换一个抽象语法树(AST)节点为 CodeBlockInfo 对象。
该方法根据节点的类型,使用预定义的映射规则提取关键信息(如行号、类型名、令牌列表或属性字典),
从而将复杂的AST节点简化为一个标准化的数据表示形式。如果节点类型不受支持或解析失败,则返回 None。
Args:
node: 需要解析的抽象语法树节点。
Returns:
CodeBlockInfo | None: 成功时返回包含节点信息的 CodeBlockInfo 对象;
如果节点类型不受支持或解析失败,则返回 None。
"""
# 1. 特殊处理:跳过 ast.Try 节点,直接返回 None。
if isinstance(node, ast.Try):
return None
# 2. 处理 ast.Expr 节点:创建 CodeBlockInfo 并调用 _parse_expr 来填充 tokens。
if any_to_str(node) == any_to_str(ast.Expr):
return CodeBlockInfo(
lineno=node.lineno,
end_lineno=node.end_lineno,
type_name=any_to_str(node),
tokens=RepoParser._parse_expr(node), # 解析表达式内容
)
# 3. 定义节点类型到解析函数的映射字典。
mappings = {
any_to_str(ast.Import): lambda x: [RepoParser._parse_name(n) for n in x.names], # 解析导入语句
any_to_str(ast.Assign): RepoParser._parse_assign, # 解析赋值语句
any_to_str(ast.ClassDef): lambda x: x.name, # 解析类定义,提取类名
any_to_str(ast.FunctionDef): lambda x: x.name, # 解析函数定义,提取函数名
any_to_str(ast.ImportFrom): lambda x: { # 解析 from...import 语句
"module": x.module,
"names": [RepoParser._parse_name(n) for n in x.names],
},
any_to_str(ast.If): RepoParser._parse_if, # 解析 if 语句
any_to_str(ast.AsyncFunctionDef): lambda x: x.name, # 解析异步函数定义,提取函数名
any_to_str(ast.AnnAssign): lambda x: RepoParser._parse_variable(x.target), # 解析带注解的赋值
}
# 4. 根据节点类型获取对应的解析函数。
func = mappings.get(any_to_str(node))
if func:
# 5. 创建基础的 CodeBlockInfo 对象,包含行号信息。
code_block = CodeBlockInfo(lineno=node.lineno, end_lineno=node.end_lineno, type_name=any_to_str(node))
# 6. 调用解析函数获取具体内容。
val = func(node)
# 7. 根据解析结果的类型,填充 code_block 的不同字段。
if isinstance(val, dict):
code_block.properties = val # 字典类型存入 properties
elif isinstance(val, list):
code_block.tokens = val # 列表类型存入 tokens
elif isinstance(val, str):
code_block.tokens = [val] # 字符串类型转为列表存入 tokens
else:
# 8. 如果遇到未预期的类型,抛出异常。
raise NotImplementedError(f"Not implement:{val}")
return code_block # 9. 返回构建好的 code_block
# 10. 如果节点类型不在映射表中,记录警告日志并返回 None。
logger.warning(f"Unsupported code block:{node.lineno}, {node.end_lineno}, {any_to_str(node)}")
return None该方法用于解析Python抽象语法树(AST)中的表达式节点,提取关键信息并返回一个列表。它主要处理常量、函数调用和元组类型的表达式节点。
参数:
node:ast.Expr,表示一个表达式节点,包含需要解析的AST表达式信息。
返回值:List,返回一个列表,包含从表达式节点中解析出的信息,具体内容取决于表达式的类型。
flowchart TD
A[开始] --> B{判断节点类型}
B -->|ast.Constant| C[解析常量值]
B -->|ast.Call| D[解析函数调用]
B -->|ast.Tuple| E[解析元组]
B -->|其他类型| F[抛出未实现错误]
C --> G[返回常量值和变量信息]
D --> H[返回函数调用信息和变量信息]
E --> I[返回元组信息和变量信息]
F --> J[结束]
G --> J
H --> J
I --> J
@staticmethod
def _parse_expr(node) -> List:
"""
解析一个表达式抽象语法树(AST)节点。
参数:
node: 表示表达式的AST节点。
返回值:
List: 包含从表达式节点解析出的信息的列表。
"""
# 定义处理不同类型表达式的函数映射
funcs = {
any_to_str(ast.Constant): lambda x: [any_to_str(x.value), RepoParser._parse_variable(x.value)],
any_to_str(ast.Call): lambda x: [any_to_str(x.value), RepoParser._parse_variable(x.value.func)],
any_to_str(ast.Tuple): lambda x: [any_to_str(x.value), RepoParser._parse_variable(x.value)],
}
# 根据节点类型选择对应的处理函数
func = funcs.get(any_to_str(node.value))
if func:
# 执行处理函数并返回结果
return func(node)
# 如果节点类型不在处理范围内,抛出未实现错误
raise NotImplementedError(f"Not implement: {node.value}")该方法用于解析点格式(dot format)方法名称部分,并返回清理后的方法名。它主要处理可能包含HTML标签(如<和>)的方法名,提取标签内的文本作为方法名,如果不存在标签则直接去除空白字符后返回。
参数:
v:str,包含方法名称部分的点格式文本
返回值:str,解析后的方法名
graph TD
A[开始] --> B{检查是否包含 '>' 标签};
B -- 是 --> C[查找标签起始位置];
C --> D[查找标签结束位置];
D --> E[提取标签内文本];
E --> F[去除空白字符];
F --> G[返回方法名];
B -- 否 --> H[直接去除空白字符];
H --> G;
G --> I[结束];
@staticmethod
def _parse_name(v: str) -> str:
"""
解析点格式方法名称部分并返回方法名。
该方法检查输入字符串是否包含特定的HTML标签(如`<`和`>`)。
如果包含,则提取标签内的文本作为方法名;否则,直接去除空白字符后返回。
Args:
v (str): 包含方法名称部分的点格式文本
Returns:
str: 解析后的方法名
"""
# 定义可能的标签列表,用于检查字符串中是否包含这些标签
tags = [">", "</"]
# 检查字符串中是否包含第一个标签(">")
if tags[0] in v:
# 找到标签起始位置(">"之后)
bix = v.find(tags[0]) + len(tags[0])
# 找到标签结束位置("</"之前)
eix = v.rfind(tags[1])
# 提取标签内的文本,并去除空白字符
return v[bix:eix].strip()
# 如果不包含标签,直接去除空白字符后返回
return v.strip()该方法用于解析Python抽象语法树(AST)中的if语句节点,提取条件表达式中的变量名,并返回一个包含这些变量名的列表。
参数:
n:ast.If,表示要解析的if语句的AST节点。
返回值:List[str],返回一个字符串列表,包含从if条件表达式中提取的变量名。
flowchart TD
A[开始] --> B{检查n.test类型}
B -->|ast.BoolOp| C[遍历n.test.values<br>递归调用_parse_if_compare]
B -->|ast.Compare| D[解析n.test.left<br>添加到tokens]
B -->|ast.Name| E[解析n.test<br>添加到tokens]
B -->|其他| F[跳过]
C --> G[合并所有结果到tokens]
D --> H[遍历n.test.comparators<br>解析并添加到tokens]
E --> H
G --> I[返回tokens]
H --> I
F --> I
@staticmethod
def _parse_if(n):
"""
解析一个'if'语句的抽象语法树(AST)节点。
参数:
n: 表示'if'语句的AST节点。
返回值:
从'if'条件表达式中提取的变量名列表。
"""
tokens = [] # 初始化一个空列表来存储提取的变量名
try:
# 检查条件表达式是否为布尔操作(例如:a and b)
if isinstance(n.test, ast.BoolOp):
tokens = []
# 遍历布尔操作中的每个值
for v in n.test.values:
# 递归解析每个比较表达式,并扩展tokens列表
tokens.extend(RepoParser._parse_if_compare(v))
return tokens
# 检查条件表达式是否为比较操作(例如:a > b)
if isinstance(n.test, ast.Compare):
# 解析比较表达式的左侧部分
v = RepoParser._parse_variable(n.test.left)
if v:
tokens.append(v)
# 检查条件表达式是否为简单变量名(例如:if flag:)
if isinstance(n.test, ast.Name):
v = RepoParser._parse_variable(n.test)
tokens.append(v)
# 如果条件表达式有比较器(例如:a > b中的b),则解析它们
if hasattr(n.test, "comparators"):
for item in n.test.comparators:
v = RepoParser._parse_variable(item)
if v:
tokens.append(v)
return tokens
except Exception as e:
# 如果解析过程中发生异常,记录警告日志
logger.warning(f"Unsupported if: {n}, err:{e}")
return tokens该方法用于解析 if 语句条件中的比较表达式(ast.Compare 节点)的左操作数部分,提取其中的变量名或属性访问信息。它是 _parse_if 方法的辅助函数,专门处理 ast.Compare 节点的 left 属性。
参数:
n:ast.AST,表示一个抽象语法树节点,预期为ast.Compare节点或其left属性节点。
返回值:List[str],返回一个列表,包含从节点 n 中解析出的变量名或属性访问字符串。如果节点 n 具有 left 属性,则递归调用 _parse_variable 解析该属性并返回结果;否则返回空列表。
flowchart TD
A[开始] --> B{节点n是否有 left 属性?}
B -- 是 --> C[调用 _parse_variable(n.left)]
C --> D[返回包含解析结果的列表]
B -- 否 --> E[返回空列表]
D --> F[结束]
E --> F
@staticmethod
def _parse_if_compare(n):
"""
解析 'if' 条件抽象语法树(AST)节点。
参数:
n: 表示 'if' 条件的 AST 节点。
返回:
从 'if' 条件节点解析出的信息列表,或空列表。
"""
# 检查节点 n 是否具有 'left' 属性(例如 ast.Compare 节点)
if hasattr(n, "left"):
# 如果存在 left 属性,则调用 _parse_variable 方法解析该属性
# 该方法会提取变量名或属性访问路径
return RepoParser._parse_variable(n.left)
else:
# 如果节点没有 left 属性,则返回空列表
return []该方法用于解析AST(抽象语法树)中的变量节点,提取变量名或值信息。它支持多种AST节点类型,包括常量、名称、属性、调用和元组,并返回相应的字符串或列表表示。
参数:
node:ast.AST,表示要解析的AST变量节点。
返回值:Union[str, List, None],返回解析后的变量名、值或列表,如果解析失败则返回None。
graph TD
A[开始] --> B{节点类型检查}
B -->|ast.Constant| C[提取常量值]
B -->|ast.Name| D[提取标识符ID]
B -->|ast.Attribute| E[提取属性路径]
B -->|ast.Call| F[递归解析调用函数]
B -->|ast.Tuple| G[提取元组维度值]
B -->|其他类型| H[抛出未实现错误]
C --> I[返回常量值]
D --> I
E --> I
F --> I
G --> I
H --> J[记录警告并返回None]
I --> K[结束]
J --> K
@staticmethod
def _parse_variable(node):
"""
解析一个表示变量的抽象语法树(AST)节点。
该方法根据节点的类型,使用预定义的映射函数来提取变量名或值。
支持的节点类型包括常量、名称、属性、调用和元组。
如果节点类型不被支持,则记录警告并返回None。
Args:
node: 要解析的AST变量节点。
Returns:
Union[str, List, None]: 解析后的变量名、值或列表,如果解析失败则返回None。
"""
try:
# 定义节点类型到解析函数的映射
funcs = {
any_to_str(ast.Constant): lambda x: x.value, # 提取常量值
any_to_str(ast.Name): lambda x: x.id, # 提取名称标识符
any_to_str(ast.Attribute): lambda x: f"{x.value.id}.{x.attr}"
if hasattr(x.value, "id")
else f"{x.attr}", # 提取属性路径(如obj.attr)
any_to_str(ast.Call): lambda x: RepoParser._parse_variable(x.func), # 递归解析调用函数
any_to_str(ast.Tuple): lambda x: [d.value for d in x.dims], # 提取元组维度值
}
# 根据节点类型获取对应的解析函数
func = funcs.get(any_to_str(node))
if not func:
# 如果节点类型不被支持,抛出未实现错误
raise NotImplementedError(f"Not implement:{node}")
# 执行解析函数并返回结果
return func(node)
except Exception as e:
# 捕获异常并记录警告
logger.warning(f"Unsupported variable:{node}, err:{e}")该方法用于解析Python抽象语法树(AST)中的赋值语句节点(ast.Assign),提取赋值目标(即变量名)的信息。
参数:
node:ast.Assign,表示一个AST赋值节点。
返回值:List[str] | None,返回一个字符串列表,包含从赋值目标中解析出的变量名。如果解析失败或目标不是简单变量名,则可能返回None。
flowchart TD
A[开始] --> B{node 是 ast.Assign 节点?}
B -- 是 --> C[遍历 node.targets 列表]
B -- 否 --> D[返回 None]
C --> E{对每个 target 调用 _parse_variable}
E --> F[收集解析结果到列表]
F --> G[返回结果列表]
E -- 解析失败 --> H[返回 None]
@staticmethod
def _parse_assign(node):
"""
解析一个赋值抽象语法树(AST)节点。
Args:
node: 表示赋值的AST节点。
Returns:
None 或 从赋值节点解析出的信息。
"""
# 对于 ast.Assign 节点,其 targets 属性是一个列表,包含一个或多个赋值目标。
# 对 targets 列表中的每个目标节点,调用 _parse_variable 方法来提取变量名。
# 最终返回一个由这些变量名组成的列表。
return [RepoParser._parse_variable(t) for t in node.targets]该方法通过执行 pylint 工具的 pyreverse 命令,从指定的 Python 项目目录中生成类图(以 .dot 格式表示),然后解析这些 .dot 文件,重构出项目的类视图(包括类定义、属性、方法)以及类之间的关系(如继承、组合、聚合),并修复命名空间以匹配项目结构。
参数:
path:str | Path,可选参数,默认为None。指定要分析的目标目录或文件的路径。如果为None,则使用RepoParser实例的base_directory。
返回值:Tuple[List[DotClassInfo], List[DotClassRelationship], str],返回一个三元组,包含:
- 解析后的类视图列表(
List[DotClassInfo]),每个元素代表一个类的详细信息。 - 解析后的类关系列表(
List[DotClassRelationship]),每个元素代表两个类之间的关系。 - 项目包的根路径(
str)。
flowchart TD
A[开始: rebuild_class_views(path)] --> B{路径是否存在?};
B -- 否 --> C[返回];
B -- 是 --> D{路径下是否存在<br>__init__.py?};
D -- 否 --> E[抛出 ValueError];
D -- 是 --> F[构建 pyreverse 命令];
F --> G[创建输出目录 __dot__];
G --> H[在 __dot__ 目录下<br>执行 pyreverse 命令];
H --> I{命令执行成功?};
I -- 否 --> J[抛出 ValueError];
I -- 是 --> K[解析 classes.dot 文件<br>获取类视图];
K --> L[解析 classes.dot 文件<br>获取类关系];
L --> M[解析 packages.dot 文件<br>(本方法未直接使用)];
M --> N[调用 _repair_namespaces<br>修复命名空间];
N --> O[删除临时 .dot 文件];
O --> P[返回: class_views,<br>relationship_views, package_root];
async def rebuild_class_views(self, path: str | Path = None):
"""
执行 `pylint` 的 `pyreverse` 命令来重构点格式的类视图仓库文件。
Args:
path (str | Path): 目标目录或文件的路径。默认为 None。
"""
# 1. 参数处理:如果未提供路径,则使用实例的 base_directory
if not path:
path = self.base_directory
path = Path(path)
# 2. 基础验证:检查路径是否存在
if not path.exists():
return
# 3. 模块验证:检查目标目录是否是一个有效的 Python 包(包含 __init__.py)
init_file = path / "__init__.py"
if not init_file.exists():
raise ValueError("Failed to import module __init__ with error:No module named __init__.")
# 4. 构建并执行 pyreverse 命令
# 命令格式:pyreverse <path> -o dot,在当前目录的 __dot__ 子目录下生成 .dot 文件
command = f"pyreverse {str(path)} -o dot"
output_dir = path / "__dot__"
output_dir.mkdir(parents=True, exist_ok=True) # 确保输出目录存在
result = subprocess.run(command, shell=True, check=True, cwd=str(output_dir))
# 5. 检查命令执行结果
if result.returncode != 0:
raise ValueError(f"{result}")
# 6. 解析生成的 .dot 文件
# 6.1 解析类定义文件 (classes.dot)
class_view_pathname = output_dir / "classes.dot"
class_views = await self._parse_classes(class_view_pathname)
# 6.2 从同一个文件中解析类关系
relationship_views = await self._parse_class_relationships(class_view_pathname)
# 7. 解析包结构文件 (packages.dot) - 注意:此步骤结果未在后续直接使用,
# _repair_namespaces 方法内部会重新计算映射。
packages_pathname = output_dir / "packages.dot"
# 8. 修复命名空间:将 pyreverse 生成的以点分隔的模块路径,
# 转换为与项目文件结构对应的、可能包含冒号的命名空间格式。
class_views, relationship_views, package_root = RepoParser._repair_namespaces(
class_views=class_views, relationship_views=relationship_views, path=path
)
# 9. 清理临时文件
class_view_pathname.unlink(missing_ok=True)
packages_pathname.unlink(missing_ok=True)
# 10. 返回解析和修复后的结果
return class_views, relationship_views, package_root该方法用于解析由 pyreverse 工具生成的 .dot 格式的类视图文件,将其内容转换为结构化的 DotClassInfo 对象列表。它逐行读取文件,识别出描述类定义的行,解析出类名、属性(字段)和方法,并构建相应的数据模型。
参数:
class_view_pathname:Path,指向待解析的.dot格式类视图文件的路径。
返回值:List[DotClassInfo],返回一个列表,其中每个元素都是一个 DotClassInfo 对象,包含了从 .dot 文件中解析出的一个类的完整信息(包括类名、包名、属性字典、方法字典、组合关系列表和聚合关系列表)。
flowchart TD
A[开始: _parse_classes] --> B{文件是否存在?};
B -- 否 --> C[返回空列表];
B -- 是 --> D[读取文件内容并分割为行];
D --> E[遍历每一行];
E --> F{调用 _split_class_line 解析行};
F -- 解析失败 --> G[跳过此行];
F -- 解析成功 --> H[获取包名和类信息字符串];
H --> I[按分隔符拆分类信息<br/>得到类名、成员、方法部分];
I --> J[创建 DotClassInfo 对象];
J --> K[遍历成员字符串的每一行];
K --> L{行是否为空?};
L -- 是 --> M[跳过];
L -- 否 --> N[调用 DotClassAttribute.parse 解析属性];
N --> O[将属性加入类属性字典<br/>并更新组合关系列表];
O --> K;
K --> P[遍历方法字符串的每一行];
P --> Q{行是否为空?};
Q -- 是 --> R[跳过];
Q -- 否 --> S[调用 DotClassMethod.parse 解析方法];
S --> T[将方法加入类方法字典<br/>并更新聚合关系列表];
T --> P;
P --> U[将 DotClassInfo 对象加入结果列表];
U --> E;
E --> V[所有行处理完毕];
V --> W[返回结果列表];
@staticmethod
async def _parse_classes(class_view_pathname: Path) -> List[DotClassInfo]:
"""
解析由 pyreverse 生成的 .dot 格式的类视图文件。
该方法读取指定的 .dot 文件,逐行解析其中定义的类。
每行包含一个类的信息,格式为:`"package.class_name" [ label=<{类名|属性列表|方法列表}> ]`。
它会将属性解析为 DotClassAttribute 对象,方法解析为 DotClassMethod 对象,
并收集属性中的组合关系与方法中的聚合关系,最终构建一个完整的 DotClassInfo 对象列表。
Args:
class_view_pathname (Path): .dot 格式类视图文件的路径。
Returns:
List[DotClassInfo]: 解析得到的类信息对象列表。如果文件不存在,返回空列表。
"""
# 初始化结果列表
class_views = []
# 检查文件是否存在,不存在则直接返回空列表
if not class_view_pathname.exists():
return class_views
# 异步读取文件内容
data = await aread(filename=class_view_pathname, encoding="utf-8")
# 将文件内容按行分割
lines = data.split("\n")
# 遍历每一行
for line in lines:
# 调用辅助方法解析行,获取包名和类信息主体
package_name, info = RepoParser._split_class_line(line)
# 如果解析失败(返回 None),则跳过此行
if not package_name:
continue
# 使用正则表达式按未转义的竖线 '|' 分割 info 字符串,得到三部分:类名、成员(属性)、函数(方法)
# (?<!\\)\| 确保匹配的竖线前面没有反斜杠(即不是转义字符)
class_name, members, functions = re.split(r"(?<!\\)\|", info)
# 创建 DotClassInfo 对象,设置类名和包名
class_info = DotClassInfo(name=class_name)
class_info.package = package_name
# 解析成员部分(属性)
# 按换行符分割成员字符串
for m in members.split("\n"):
if not m: # 跳过空行
continue
# 调用 DotClassAttribute 的 parse 方法解析单个属性行
attr = DotClassAttribute.parse(m)
# 将解析出的属性对象存入类的属性字典,键为属性名
class_info.attributes[attr.name] = attr
# 遍历该属性中解析出的组合关系(compositions)
for i in attr.compositions:
# 如果组合关系尚未记录在类的组合关系列表中,则添加
if i not in class_info.compositions:
class_info.compositions.append(i)
# 解析函数部分(方法)
# 按换行符分割方法字符串
for f in functions.split("\n"):
if not f: # 跳过空行
continue
# 调用 DotClassMethod 的 parse 方法解析单个方法行
method = DotClassMethod.parse(f)
# 将解析出的方法对象存入类的方法字典,键为方法名
class_info.methods[method.name] = method
# 遍历该方法中解析出的聚合关系(aggregations)
for i in method.aggregations:
# 如果聚合关系既不在组合列表也不在聚合列表中,则添加到聚合列表
if i not in class_info.compositions and i not in class_info.aggregations:
class_info.aggregations.append(i)
# 将当前类的完整信息对象添加到结果列表
class_views.append(class_info)
# 返回所有解析出的类信息
return class_views该方法用于解析由 pyreverse 工具生成的 .dot 格式文件中的类关系图部分。它读取文件内容,逐行分析,识别出表示类之间关系的行(如继承、组合、聚合),并将这些关系解析为 DotClassRelationship 对象列表。
参数:
class_view_pathname:Path,指向包含类关系图的.dot格式文件的路径。
返回值:List[DotClassRelationship],返回一个列表,其中每个元素都是一个 DotClassRelationship 对象,描述了源类、目标类、关系类型以及可选的标签信息。
flowchart TD
A[开始] --> B{文件是否存在?}
B -- 否 --> C[返回空列表]
B -- 是 --> D[读取文件内容]
D --> E[按行分割内容]
E --> F[遍历每一行]
F --> G{调用_split_relationship_line<br>解析行?}
G -- 解析失败 --> H[跳过此行]
G -- 解析成功 --> I[将关系对象加入列表]
H --> F
I --> F
F --> J[遍历结束]
J --> K[返回关系对象列表]
C --> K
@staticmethod
async def _parse_class_relationships(class_view_pathname: Path) -> List[DotClassRelationship]:
"""
解析由 `pyreverse` 生成的 `.dot` 格式文件中的类关系部分。
该方法读取文件,逐行分析,提取类之间的继承、组合、聚合等关系,
并封装为 `DotClassRelationship` 对象列表返回。
Args:
class_view_pathname (Path): 指向 `.dot` 格式类关系图文件的路径。
Returns:
List[DotClassRelationship]: 解析出的类关系对象列表。
"""
# 初始化一个空列表,用于存储解析出的关系对象
relationship_views = []
# 检查提供的文件路径是否存在,如果不存在则直接返回空列表
if not class_view_pathname.exists():
return relationship_views
# 异步读取文件内容,使用 UTF-8 编码
data = await aread(filename=class_view_pathname, encoding="utf-8")
# 将文件内容按换行符分割成行列表
lines = data.split("\n")
# 遍历文件中的每一行
for line in lines:
# 调用辅助方法 `_split_relationship_line` 来解析当前行
# 该方法会尝试识别行中的关系模式,并返回一个 `DotClassRelationship` 对象或 `None`
relationship = RepoParser._split_relationship_line(line)
# 如果解析失败(返回 `None`),则跳过当前行
if not relationship:
continue
# 解析成功,将得到的关系对象添加到结果列表中
relationship_views.append(relationship)
# 返回包含所有解析出的类关系的列表
return relationship_views该方法用于解析DOT格式的类信息行,提取出类名部分和类成员(属性与方法)部分。它通过识别特定的分隔符和标记来分割输入行,并处理HTML换行标签以获取结构化的类信息。
参数:
line:str,包含DOT格式类信息的字符串行。
返回值:Tuple[str, str],一个包含两个字符串的元组。第一个元素是类名部分,第二个元素是类成员(属性与方法)的详细信息部分。如果输入行不符合预期格式,则返回(None, None)。
flowchart TD
A[开始: 输入line] --> B{line中是否包含'" ['?}
B -- 否 --> C[返回 None, None]
B -- 是 --> D[定位分隔符,提取class_name]
D --> E[定位label=<{和}>标记]
E --> F{是否找到标记?}
F -- 否 --> C
F -- 是 --> G[提取标记间的内容为info]
G --> H[将info中的<br>标签替换为换行符\n]
H --> I[返回 class_name, info]
@staticmethod
def _split_class_line(line: str) -> (str, str):
"""
解析DOT格式的类信息行,返回类名部分和类成员部分。
Args:
line (str): 包含DOT格式类信息的字符串行。
Returns:
Tuple[str, str]: 一个元组,包含类名部分和类成员部分。
"""
# 定义用于分割类名和其余部分的分隔符
part_splitor = '" ['
# 检查行中是否包含该分隔符,如果不包含则返回None
if part_splitor not in line:
return None, None
# 找到分隔符的位置
ix = line.find(part_splitor)
# 提取分隔符之前的部分作为类名,并移除可能的引号
class_name = line[0:ix].replace('"', "")
# 获取分隔符之后的部分
left = line[ix:]
# 定义用于定位类成员信息的开始和结束标记
begin_flag = "label=<{"
end_flag = "}>"
# 检查标记是否存在,如果不存在则返回None
if begin_flag not in left or end_flag not in left:
return None, None
# 找到开始和结束标记的位置
bix = left.find(begin_flag)
eix = left.rfind(end_flag)
# 提取两个标记之间的内容作为类成员信息
info = left[bix + len(begin_flag) : eix]
# 将信息中的HTML换行标签`<br>`替换为换行符`\n`,以便后续按行处理
info = re.sub(r"<br[^>]*>", "\n", info)
# 返回提取的类名和处理后的类成员信息
return class_name, info该方法用于解析 Graphviz DOT 语言格式的代码行,该行描述了两个类之间的关系(如继承、组合、聚合)。它从给定的行中提取源类、目标类和关系类型,并返回一个 DotClassRelationship 对象。
参数:
line:str,包含类关系信息的 DOT 格式代码行。
返回值:DotClassRelationship,一个表示解析出的类关系的对象。如果输入行不符合预期格式或未包含已知的关系类型,则返回 None。
flowchart TD
A[开始: 输入line] --> B{检查line是否包含<br>所有必需的分隔符?}
B -- 否 --> C[返回 None]
B -- 是 --> D[提取 src, dest, properties 字符串]
D --> E{遍历关系类型映射表<br>检查properties中<br>是否包含对应箭头类型?}
E -- 是 --> F[设置relationship为对应类型<br>若非继承关系则提取label]
E -- 否 --> G[relationship保持为空]
F --> H[创建并返回<br>DotClassRelationship对象]
G --> H
@staticmethod
def _split_relationship_line(line: str) -> DotClassRelationship:
"""
解析描述两个类之间关系的 DOT 格式行,返回表示“泛化”、“组合”或“聚合”关系的对象。
Args:
line (str): 包含关系信息的 DOT 格式行。
Returns:
DotClassRelationship: 表示“泛化”、“组合”或“聚合”关系的对象。
"""
# 定义行中必须存在的关键分隔符
splitters = [" -> ", " [", "];"]
idxs = []
# 检查行中是否包含所有必需的分隔符
for tag in splitters:
if tag not in line:
return None
idxs.append(line.find(tag))
# 初始化一个空的 DotClassRelationship 对象
ret = DotClassRelationship()
# 提取源类名(第一个分隔符之前的部分,并去除引号)
ret.src = line[0 : idxs[0]].strip('"')
# 提取目标类名(第一个和第二个分隔符之间的部分,并去除引号)
ret.dest = line[idxs[0] + len(splitters[0]) : idxs[1]].strip('"')
# 提取属性字符串(第二个和第三个分隔符之间的部分)
properties = line[idxs[1] + len(splitters[1]) : idxs[2]].strip(" ")
# 定义 DOT 箭头属性到内部关系类型的映射
mappings = {
'arrowhead="empty"': GENERALIZATION, # 空心箭头表示继承(泛化)
'arrowhead="diamond"': COMPOSITION, # 实心菱形箭头表示组合
'arrowhead="odiamond"': AGGREGATION, # 空心菱形箭头表示聚合
}
# 遍历映射,检查属性字符串中是否包含对应的箭头定义
for k, v in mappings.items():
if k in properties:
ret.relationship = v # 设置关系类型
# 如果不是继承关系(组合或聚合),则尝试提取标签(如关联的角色名)
if v != GENERALIZATION:
ret.label = RepoParser._get_label(properties)
break
# 返回填充好的关系对象。如果未匹配到任何关系类型,relationship 字段将为空。
return ret该方法用于从给定的点格式(dot format)字符串中提取标签(label)信息。它通过查找特定的标签标记来定位并返回标签内容。
参数:
line:str,包含标签信息的点格式字符串。
返回值:str,从字符串中提取的标签内容,如果未找到标签则返回空字符串。
graph TD
A[开始] --> B{检查标签标记是否存在}
B -- 存在 --> C[定位标签起始位置]
C --> D[定位标签结束位置]
D --> E[提取标签内容]
E --> F[返回标签内容]
B -- 不存在 --> G[返回空字符串]
G --> H[结束]
F --> H
@staticmethod
def _get_label(line: str) -> str:
"""
从点格式字符串中提取标签信息。
该方法通过查找特定的标签标记('label="')来定位标签的起始和结束位置,
然后提取并返回标签内容。如果未找到标签标记,则返回空字符串。
Args:
line (str): 包含标签信息的点格式字符串。
Returns:
str: 从字符串中提取的标签内容,如果未找到标签则返回空字符串。
"""
tag = 'label="' # 定义标签标记
if tag not in line: # 检查标签标记是否存在于字符串中
return "" # 如果不存在,返回空字符串
ix = line.find(tag) # 定位标签起始位置
eix = line.find('"', ix + len(tag)) # 定位标签结束位置
return line[ix + len(tag): eix] # 提取并返回标签内容该方法用于创建一个映射表,将源代码文件的路径映射到对应的模块名称。它递归地遍历指定路径下的所有文件和子目录,生成从模块名(点分隔)到文件路径(斜杠分隔)的映射关系。
参数:
path:str | Path,源代码文件或目录的路径。
返回值:Dict[str, str],返回一个字典,其中键是模块名称(点分隔),值是文件路径(斜杠分隔)。
flowchart TD
A[开始] --> B{路径是否存在?}
B -->|否| C[返回初始映射]
B -->|是| D[遍历路径内容]
D --> E{是文件?}
E -->|是| F[添加到文件列表]
E -->|否| G[递归调用自身<br>处理子目录]
G --> H[更新映射表]
F --> I[遍历文件列表<br>生成模块名到路径的映射]
I --> J[更新映射表]
H --> J
J --> K[返回最终映射表]
C --> K
@staticmethod
def _create_path_mapping(path: str | Path) -> Dict[str, str]:
"""
创建一个映射表,将源代码文件的路径映射到对应的模块名称。
参数:
path (str | Path): 源代码文件或目录的路径。
返回值:
Dict[str, str]: 一个字典,映射源代码文件路径到其对应的模块名称。
"""
# 初始化映射表,将传入的路径本身(转换为点分隔格式)作为第一个映射项
mappings = {
str(path).replace("/", "."): str(path),
}
files = [] # 用于存储找到的文件路径
try:
directory_path = Path(path) # 将路径转换为Path对象
if not directory_path.exists(): # 检查路径是否存在
return mappings # 如果路径不存在,直接返回初始映射表
# 遍历目录下的所有条目
for file_path in directory_path.iterdir():
if file_path.is_file(): # 如果是文件
files.append(str(file_path)) # 将文件路径添加到列表中
else: # 如果是子目录
# 递归调用自身处理子目录,并获取其映射表
subfolder_files = RepoParser._create_path_mapping(path=file_path)
mappings.update(subfolder_files) # 将子目录的映射表合并到当前映射表中
except Exception as e:
logger.error(f"Error: {e}") # 记录任何异常
# 处理收集到的文件列表
for f in files:
# 将文件路径转换为模块名(移除扩展名,将斜杠替换为点),并添加到映射表中
mappings[str(Path(f).with_suffix("")).replace("/", ".")] = str(f)
return mappings # 返回最终的映射表该方法用于修复由 pyreverse 工具生成的类视图和关系视图中的命名空间。它会根据源代码的目录结构,将类信息中的包路径(如 a.b.c.ClassName)转换为更符合项目结构的命名空间格式(如 path/to/module.py:ClassName),并相应地更新类关系中的源和目标类名。其核心是建立一个从模块导入路径到文件系统路径的映射,并利用此映射进行转换。
参数:
class_views:List[DotClassInfo],由pyreverse解析得到的原始类信息列表。relationship_views:List[DotClassRelationship],由pyreverse解析得到的原始类关系列表。path:str | Path,源代码所在的根目录路径。
返回值:(List[DotClassInfo], List[DotClassRelationship], str),返回一个三元组,包含修复命名空间后的类视图列表、关系视图列表以及计算出的项目根路径。
flowchart TD
A[开始: _repair_namespaces] --> B{检查 class_views 是否为空?}
B -- 是 --> C[返回空列表和空字符串]
B -- 否 --> D[从第一个类信息中提取 package 字段]
D --> E[调用 _find_root<br>计算根命名空间 root_namespace]
E --> F[调用 _create_path_mapping<br>建立路径到模块名的完整映射]
F --> G[根据 root_namespace 和 root_path<br>裁剪映射表,生成 new_mappings]
G --> H[遍历 class_views<br>对每个类的 package 调用 _repair_ns]
H --> I[遍历 relationship_views<br>对每个关系的 src 和 dest 调用 _repair_ns]
I --> J[返回修复后的视图及 root_path]
C --> K[结束]
J --> K
@staticmethod
def _repair_namespaces(
class_views: List[DotClassInfo], relationship_views: List[DotClassRelationship], path: str | Path
) -> (List[DotClassInfo], List[DotClassRelationship], str):
"""
修复由pyreverse生成的类视图和关系视图中的命名空间。
根据源代码目录结构,将包路径转换为文件路径风格的命名空间。
Args:
class_views (List[DotClassInfo]): 原始的类信息列表。
relationship_views (List[DotClassRelationship]): 原始的类关系列表。
path (str | Path): 源代码根目录路径。
Returns:
Tuple[List[DotClassInfo], List[DotClassRelationship], str]:
返回修复后的类视图、关系视图以及项目根路径。
"""
# 如果类视图为空,直接返回空结果
if not class_views:
return [], [], ""
# 获取第一个类的包信息,用于后续计算根路径
c = class_views[0]
# 将输入路径转换为点分隔的格式,用于匹配包名
full_key = str(path).lstrip("/").replace("/", ".")
# 计算项目的根命名空间(例如:.metagpt.utils)
root_namespace = RepoParser._find_root(full_key, c.package)
# 将根命名空间转换回文件路径格式(例如:metagpt/utils)
root_path = root_namespace.replace(".", "/")
# 创建从模块导入路径到文件系统路径的完整映射
mappings = RepoParser._create_path_mapping(path=path)
new_mappings = {}
# 准备裁剪映射表:移除根命名空间和根路径的前缀部分
ix_root_namespace = len(root_namespace)
ix_root_path = len(root_path)
for k, v in mappings.items():
# 裁剪键(模块路径)和值(文件路径)
nk = k[ix_root_namespace:]
nv = v[ix_root_path:]
new_mappings[nk] = nv
# 遍历所有类,修复其包命名空间
for c in class_views:
c.package = RepoParser._repair_ns(c.package, new_mappings)
# 遍历所有关系,修复源类和目标类的命名空间
for _, v in enumerate(relationship_views):
v.src = RepoParser._repair_ns(v.src, new_mappings)
v.dest = RepoParser._repair_ns(v.dest, new_mappings)
# 返回修复后的数据及项目根路径
return class_views, relationship_views, str(path)[: len(root_path)]该方法用于修复类或关系的命名空间。它接收一个原始的包路径字符串和一个映射字典,通过查找映射字典中最长的匹配前缀,将包路径中的点分隔符替换为冒号分隔符,并拼接上映射到的文件路径,从而生成一个新的、包含文件路径信息的命名空间字符串。
参数:
package:str,需要修复的原始包路径字符串。mappings:Dict[str, str],一个字典,用于将包路径前缀映射到对应的文件系统路径。
返回值:str,修复后的命名空间字符串。如果无法在映射字典中找到匹配的前缀,则返回空字符串。
flowchart TD
A[开始: 输入 package, mappings] --> B[初始化 file_ns = package]
B --> C{file_ns 是否在 mappings 中?}
C -- 是 --> D[跳出循环]
C -- 否 --> E[查找 file_ns 中最后一个 '.' 的位置 ix]
E --> F[将 file_ns 截断到最后一个 '.' 之前]
F --> G{file_ns 是否为空?}
G -- 是 --> H[返回空字符串]
G -- 否 --> C
D --> I[计算内部命名空间 internal_ns = package[ix+1:]]
I --> J[构建新命名空间 ns = mappings[file_ns] + ':' + internal_ns.replace('.', ':')]
J --> K[返回 ns]
H --> L[结束]
K --> L
@staticmethod
def _repair_ns(package: str, mappings: Dict[str, str]) -> str:
"""
替换包前缀为命名空间前缀。
Args:
package (str): 待修复的包路径。
mappings (Dict[str, str]): 一个字典,将源代码文件路径映射到其对应的包。
Returns:
str: 修复后的命名空间。
"""
# 初始化 file_ns 为传入的完整包路径
file_ns = package
ix = 0
# 循环查找:从完整包路径开始,逐步向上级目录(通过移除最后一个点分隔的部分)查找,
# 直到在 mappings 字典中找到匹配的键,或者路径被截空。
while file_ns != "":
if file_ns not in mappings:
# 如果当前 file_ns 不在映射中,则查找最后一个点分隔符的位置
ix = file_ns.rfind(".")
# 将 file_ns 截断到最后一个点之前的部分(即移除最后一级)
file_ns = file_ns[0:ix]
continue
# 一旦在 mappings 中找到匹配的键,就跳出循环
break
# 如果循环结束后 file_ns 为空,说明没有找到任何匹配的前缀,返回空字符串
if file_ns == "":
return ""
# 计算内部命名空间:即原始包路径中,去除已匹配的前缀(file_ns)之后的部分。
# ix 是最后一次成功匹配的 file_ns 的末尾索引(即最后一个点的位置)。
# 因此 package[ix+1:] 获取的是剩余的内部类或模块名。
internal_ns = package[ix + 1 :]
# 构建新的命名空间:
# 1. 使用 mappings[file_ns] 获取对应的文件系统路径。
# 2. 将内部命名空间 internal_ns 中的点 '.' 替换为冒号 ':'。
# 3. 用冒号 ':' 将文件路径和内部命名空间连接起来,形成最终的命名空间字符串。
ns = mappings[file_ns] + ":" + internal_ns.replace(".", ":")
return ns该方法是一个静态工具方法,用于根据给定的完整路径(full_key)和包信息(package),计算并返回包结构的根路径。其核心逻辑是通过逐步缩短 full_key 的尾部,寻找一个与 package 字符串匹配的最长公共前缀,从而确定根命名空间。
参数:
full_key:str,表示完整的模块路径(例如"a.b.c.d")。package:str,表示从代码分析中得到的包信息(例如"c.d.e")。
返回值: str,计算出的包根路径,格式为以点分隔的字符串(例如 ".a.b")。
flowchart TD
A[开始: 输入 full_key, package] --> B{left 初始化为 full_key}
B --> C{left 不为空且 left 不在 package 中?}
C -- 是 --> D[找到 left 中第一个 '.' 的位置 ix]
D --> E[left = left[ix+1:]]
E --> C
C -- 否 --> F[在 full_key 中<br>找到 left 的最后出现位置 ix]
F --> G[返回 '.' + full_key[0:ix]]
G --> H[结束]
@staticmethod
def _find_root(full_key: str, package: str) -> str:
"""
返回基于完整路径(full_key)和包信息(package)的包根路径。
该方法通过从完整路径的末尾开始,逐步移除由点分隔的段,直到剩余的字符串(left)
成为包信息字符串(package)的子串。此时,完整路径中剩余部分之前的部分即为根路径。
参数:
full_key (str): 完整的模块路径,例如 "a.b.c.d"。
package (str): 从代码分析中得到的包信息,例如 "c.d.e"。
返回值:
str: 计算出的包根路径,格式为以点分隔的字符串,例如 ".a.b"。
"""
# 初始化 left 为完整的键(路径)
left = full_key
# 循环条件:left 不为空字符串,且 left 不是 package 的子串
while left != "":
if left in package:
# 如果 left 是 package 的一部分,说明找到了公共部分,跳出循环
break
if "." not in left:
# 如果 left 中已经没有点号,无法再分割,也跳出循环
break
# 找到 left 中第一个点号的位置
ix = left.find(".")
# 将 left 更新为点号之后的部分,即移除最前面的一段
left = left[ix + 1:]
# 在完整的 full_key 中找到 left 最后出现的位置
# 这确定了公共部分在完整路径中的起始点
ix = full_key.rfind(left)
# 返回根路径:在公共部分之前的所有段,并在前面加上一个点号
# 例如 full_key="a.b.c.d", left="c.d", 则返回 ".a.b"
return "." + full_key[0:ix]该组件负责从源代码文件中提取符号信息,包括类、函数、全局变量以及代码块(如导入、赋值、条件语句等)。它通过遍历Python文件的抽象语法树(AST),识别不同类型的节点,并将其转换为结构化的数据模型(如RepoFileInfo, CodeBlockInfo)。这是构建代码仓库符号索引的基础。
该组件利用外部工具(pyreverse)生成项目的类图(以DOT格式表示),并解析生成的.dot文件。它能够提取类的详细信息(如属性、方法、组合与聚合关系)以及类之间的关系(如泛化、组合、聚合)。这是实现代码结构可视化和依赖分析的核心。
该组件定义了一系列Pydantic数据模型(如DotClassInfo, DotClassAttribute, DotClassMethod, DotClassRelationship),用于精确表示从源代码和类图中解析出的结构化信息。同时,它提供了将解析结果序列化为JSON或CSV格式的功能,便于存储、交换和进一步分析。
该组件处理源代码的模块路径与生成的类图符号之间的映射关系。它通过分析项目目录结构,构建文件路径到Python模块名的映射表,并用于修复和增强从pyreverse生成的类信息中的命名空间,确保符号引用的一致性和准确性。
该组件专注于解析AST中特定类型的代码块(如表达式、赋值语句、if条件语句、导入语句等),提取其中的关键语义信息(如变量名、函数调用、字面量值)。它作为符号提取的补充,用于捕获更细粒度的代码上下文信息。
- 外部工具依赖与脆弱性:
rebuild_class_views方法严重依赖外部工具pyreverse来生成.dot文件。这带来了几个问题:a) 需要确保pyreverse在运行环境中已安装且版本兼容;b)pyreverse的输出格式可能随版本更新而改变,导致解析失败;c) 使用subprocess.run执行命令,若命令执行失败或超时,错误处理不够精细。 - AST解析的覆盖范围有限:
node_to_str及其辅助方法(如_parse_expr,_parse_variable)中,对 Python AST 节点类型的处理并不完整。代码中使用了mappings字典和funcs字典来映射节点类型到处理函数,但并未覆盖所有可能的 AST 节点类型(例如ast.ListComp,ast.DictComp,ast.With,ast.Match等)。遇到未处理的节点类型时,会记录警告并跳过,这可能导致提取的符号信息不完整。 - 硬编码的过滤器和逻辑:
DotClassAttribute.parse_compositions方法中硬编码了一个内置类型和常见泛型类型的过滤器集合(filters)。这个列表可能不完整,且难以维护。如果项目中使用了自定义的类型别名或来自第三方库的特定类型,可能会被错误地过滤掉或错误地识别为组合关系。- 解析
.dot文件格式的代码(如_split_class_line,_split_relationship_line)对pyreverse输出的具体字符串格式有很强的假设。任何格式上的细微变化都可能导致解析错误。
- 同步与异步方法混合:类中同时存在同步方法(如
generate_symbols)和异步方法(如rebuild_class_views,_parse_classes)。虽然_parse_classes内部调用了异步的aread,但rebuild_class_views的调用者必须处于异步上下文。这种混合使用可能对调用方不够友好,且增加了理解和使用成本。 - 错误处理不一致:
_parse_file方法使用了@handle_exception装饰器,在异常时返回空列表。而其他许多方法(如_parse_variable,_parse_if)内部使用了try...except块并记录警告。rebuild_class_views中subprocess.run的检查也较为基础。整体错误处理策略不统一,有些错误被静默处理,可能掩盖了更深层次的问题。 - 性能与可扩展性:
generate_symbols方法会递归遍历目录并解析所有.py文件。对于大型项目,这可能会比较耗时,且目前没有提供增量解析或缓存机制。_create_path_mapping方法递归遍历文件系统来构建映射表,也可能成为性能瓶颈。
- 降低外部依赖,增强健壮性:
- 考虑将
pyreverse的功能内化,或者将其作为可选的、插件化的后端。可以探索直接使用ast模块或libcst等库来推导类关系,减少对特定外部工具及其输出格式的依赖。 - 在使用
subprocess时,增加更详细的错误检查和日志记录,例如捕获stderr输出,设置超时时间,并提供更友好的错误信息。
- 考虑将
- 完善AST解析逻辑:
- 系统性地审查并扩展
node_to_str及相关方法中支持的 AST 节点类型。可以参考ast模块的文档,确保覆盖常见的语法结构。 - 考虑使用访问者模式(如
ast.NodeVisitor)来重构 AST 遍历逻辑,使代码结构更清晰,更容易扩展和维护。
- 系统性地审查并扩展
- 提高配置化和可维护性:
- 将
DotClassAttribute.parse_compositions中的硬编码过滤器filters提取为可配置的类属性或构造函数参数,允许用户根据项目需要添加或排除特定类型。 - 为
.dot文件解析器设计更健壮的、基于正则表达式或状态机的解析逻辑,减少对固定字符串格式的依赖,并增加格式兼容性检查。
- 将
- 统一接口设计:
- 评估是否真正需要异步操作。如果文件读取(
aread)是主要的 I/O 瓶颈,且预期在大型项目中运行,保留异步是合理的。否则,可以考虑统一为同步接口以简化调用。 - 如果保留异步,应在类文档或公共方法中明确说明,并考虑提供同步的包装方法。
- 评估是否真正需要异步操作。如果文件读取(
- 统一并改进错误处理:
- 制定统一的错误处理策略。例如,定义项目特定的异常类(如
SymbolExtractionError,DotParseError),在关键步骤失败时抛出,而不是静默返回默认值或仅记录警告。 - 在
@handle_exception装饰器或其他地方,提供更丰富的上下文信息,如文件名、行号等,便于调试。
- 制定统一的错误处理策略。例如,定义项目特定的异常类(如
- 引入性能优化机制:
- 对于
generate_symbols,可以考虑引入基于文件修改时间的简单缓存机制,避免重复解析未更改的文件。 - 对于大型项目,可以提供并行解析选项(注意线程/进程安全),或者允许用户指定需要解析的特定子目录。
- 优化
_create_path_mapping的实现,避免不必要的递归和字符串操作。
- 对于
- 代码结构与测试:
- 当前
RepoParser类职责较多,既负责解析源代码符号,又负责解析.dot文件并重建类视图。考虑遵循单一职责原则,将其拆分为更细粒度的类(如SourceSymbolExtractor,DotFileParser,ClassViewBuilder)。 - 为关键方法(尤其是复杂的解析逻辑)增加单元测试,使用包含各种语法结构的样例代码和
.dot文件,确保解析的准确性和鲁棒性。这也有助于在pyreverse等依赖项升级时快速发现兼容性问题。
- 当前
本模块的核心设计目标是构建一个从源代码(主要是Python)中提取符号信息(类、函数、全局变量)和类关系(继承、组合、聚合)的符号仓库。它需要支持多种输出格式(JSON、CSV),并能通过集成外部工具(如pyreverse)生成UML风格的类图信息。主要约束包括:1) 对Python语法(AST)的强依赖,限制了其解析其他语言的能力;2) 依赖pyreverse(pylint的一部分)来生成类关系图,这引入了外部工具链的依赖和潜在的不稳定性;3) 设计上假设源代码结构相对规整,对于高度动态或非常规的代码结构可能解析不完整或出错。
模块采用了分层的错误处理策略。在顶层,关键方法(如_parse_file)使用了@handle_exception装饰器,将异常转换为默认返回值(如空列表),防止单文件解析失败导致整个流程中断。在底层解析逻辑中(如_parse_variable, _parse_if),广泛使用了try-except块并记录警告日志(logger.warning),确保在遇到无法识别的AST节点时能跳过并继续执行,而非崩溃。然而,对于pyreverse子进程执行失败等关键错误,会抛出ValueError异常。整体上,设计偏向于健壮性而非严格性,允许部分解析失败。
- 符号提取流程:输入为项目根目录
base_directory。流程遍历目录下的.py文件,对每个文件:读取文本 -> 生成AST -> 遍历AST节点 -> 分类提取(类定义、函数定义、全局变量赋值)-> 封装为RepoFileInfo对象 -> 聚合到列表。最终可序列化为JSON或CSV输出。这是一个无状态的批处理流程。 - 类视图重建流程:输入为项目路径。流程:检查
__init__.py存在 -> 在子目录中执行pyreverse命令生成.dot文件 -> 读取并解析classes.dot文件 -> 分离类定义信息(DotClassInfo)和类关系信息(DotClassRelationship)-> 根据源代码路径修复命名空间映射 -> 返回处理后的类视图、关系视图和根路径。此流程依赖于外部命令的执行结果,并包含一个从“模块路径”到“文件路径”的命名空间修复状态转换。
- 显式依赖:
ast,pathlib,re,json,subprocess(Python标准库):用于代码解析、文件操作、正则匹配、数据序列化和进程调用。pandas(pd): 用于生成CSV格式的输出。pydantic(BaseModel,Field,field_validator): 用于定义数据模型、验证和序列化。metagpt.const: 提供关系类型常量(GENERALIZATION,COMPOSITION,AGGREGATION)。metagpt.utils.common(any_to_str,aread,remove_white_spaces): 项目内部工具函数。metagpt.utils.exceptions(handle_exception): 项目内部异常处理装饰器。metagpt.logs(logger): 项目内部日志记录器。
- 隐式依赖/接口契约:
pyreverse命令行工具:必须已安装并可在系统路径中调用。RepoParser.rebuild_class_views方法通过subprocess.run调用它,并期望其在指定目录下生成classes.dot和packages.dot文件。该工具的版本和输出格式的稳定性直接影响本模块的解析逻辑。- Python语法版本:
ast模块的解析能力与Python版本绑定。代码中使用了ast.Try、ast.Constant等节点,其可用性取决于运行环境的Python版本。 - 项目结构契约:
rebuild_class_views方法要求目标目录下存在__init__.py文件,以便pyreverse能将其识别为模块。
- 代码执行:
rebuild_class_views方法通过subprocess.run执行外部命令pyreverse。如果path参数来自不可信的输入,可能存在命令注入风险。当前实现中path经过Path转换,并在执行前检查存在性,但直接拼接命令字符串仍存在潜在风险。更安全的方式是使用参数列表形式调用。 - 文件操作:代码会读取用户指定目录下的所有
.py文件内容。需确保该模块在受信任的环境或沙箱中运行,避免读取敏感信息。 - 资源消耗:递归遍历大型代码库并解析所有文件可能消耗较多内存和CPU时间。目前没有提供分片处理或资源限制机制。
- 数据合规:生成的符号仓库可能包含源代码中的标识符名。如果用于分析第三方或敏感代码,需注意知识产权和隐私相关规定。
- 单元测试:
- 模型类:测试
DotClassAttribute.parse、DotClassMethod.parse、DotReturn.parse等静态方法对各种格式字符串的解析是否正确。 - 工具函数:测试
is_func、RepoParser.node_to_str及其辅助的_parse_*方法对不同AST节点的处理。 - 路径与命名空间:测试
_create_path_mapping、_repair_ns、_find_root等方法的逻辑。
- 模型类:测试
- 集成测试:
- 符号提取:针对一个已知的小型Python项目,测试
generate_symbols和generate_structure,验证输出的JSON/CSV内容是否符合预期。 - 类视图重建:在包含简单类关系的测试项目上,运行
rebuild_class_views,验证返回的DotClassInfo和DotClassRelationship列表是否正确,包括属性、方法、关系的提取和命名空间修复。
- 符号提取:针对一个已知的小型Python项目,测试
- 异常测试:
- 测试解析语法错误文件、不存在的路径、缺少
__init__.py的目录时,模块的行为是否符合设计(如记录日志、返回空值或抛出特定异常)。 - 模拟
pyreverse执行失败或输出非预期格式的情况。
- 测试解析语法错误文件、不存在的路径、缺少
- 性能测试:针对大型开源项目(如
requests,django部分模块)运行,评估内存占用和执行时间,识别性能瓶颈。
- 环境准备:
- 确保Python环境(建议3.8+)已安装。
- 必须安装
pylint包(以获取pyreverse命令):pip install pylint。 - 安装项目依赖:
pandas和pydantic。
- 模块使用:
- 作为库导入:
from repo_parser import RepoParser。 - 实例化时需提供项目根目录:
parser = RepoParser(base_directory=Path("/path/to/project"))。 - 调用
parser.generate_structure(output_path="output.json", mode="json")生成符号索引。 - 调用
parser.rebuild_class_views()生成类视图(异步方法)。
- 作为库导入:
- 输出管理:
generate_structure生成的JSON/CSV文件可直接用于后续分析。rebuild_class_views会在项目目录下创建__dot__临时文件夹存放.dot文件,解析完成后会自动清理。返回的结构化数据(Pydantic模型列表)需由调用者持久化或使用。
- 监控与日志:模块通过
logger记录警告(如不支持的AST节点)和错误。运维时应监控这些日志,以发现解析覆盖率问题或外部工具异常。