1
0
forked from bot/app
This commit is contained in:
2024-08-29 13:52:19 +08:00
parent cb3ee4b72f
commit 7c0b0df6ed
13 changed files with 2 additions and 2 deletions

293
litedoc/syntax/astparser.py Normal file
View File

@ -0,0 +1,293 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/28 下午2:13
@Author : snowykami
@Email : snowykami@outlook.com
@File : astparser.py
@Software: PyCharm
"""
import ast
import inspect
from .node import *
from ..docstring.parser import parse
class AstParser:
def __init__(self, code: str):
self.code = code
self.tree = ast.parse(code)
self.classes: list[ClassNode] = []
self.functions: list[FunctionNode] = []
self.variables: list[AssignNode] = []
self.parse()
@staticmethod
def clear_quotes(s: str) -> str:
"""
去除类型注解中的引号
Args:
s:
Returns:
"""
return s.replace("'", "").replace('"', "")
def get_line_content(self, lineno: int, ignore_index_out: bool = True) -> str:
"""获取代码行内容
Args:
lineno: 行号
ignore_index_out: 是否忽略索引越界
Returns:
代码行内容
"""
if ignore_index_out:
if lineno < 1 or lineno > len(self.code.split("\n")):
return ""
return self.code.split("\n")[lineno - 1]
@staticmethod
def match_line_docs(linecontent: str) -> str:
"""匹配行内注释
Args:
linecontent: 行内容
Returns:
文档字符串
"""
in_string = False
string_char = ''
for i, char in enumerate(linecontent):
if char in ('"', "'"):
if in_string:
if char == string_char:
in_string = False
else:
in_string = True
string_char = char
elif char == '#' and not in_string:
return linecontent[i + 1:].strip()
return ""
def parse(self):
for node in ast.walk(self.tree):
if isinstance(node, ast.ClassDef):
if not self._is_module_level_class(node):
continue
class_node = ClassNode(
name=node.name,
docs=parse(ast.get_docstring(node)) if ast.get_docstring(node) else None,
inherits=[ast.unparse(base) for base in node.bases]
)
self.classes.append(class_node)
# 继续遍历类内部的函数
for sub_node in node.body:
if isinstance(sub_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
class_node.methods.append(FunctionNode(
name=sub_node.name,
docs=parse(ast.get_docstring(sub_node)) if ast.get_docstring(sub_node) else None,
posonlyargs=[
ArgNode(
name=arg.arg,
type=self.clear_quotes(ast.unparse(arg.annotation).strip()) if arg.annotation else TypeHint.NO_TYPEHINT,
)
for arg in sub_node.args.posonlyargs
],
args=[
ArgNode(
name=arg.arg,
type=self.clear_quotes(ast.unparse(arg.annotation).strip()) if arg.annotation else TypeHint.NO_TYPEHINT,
)
for arg in sub_node.args.args
],
kwonlyargs=[
ArgNode(
name=arg.arg,
type=self.clear_quotes(ast.unparse(arg.annotation).strip()) if arg.annotation else TypeHint.NO_TYPEHINT,
)
for arg in sub_node.args.kwonlyargs
],
kw_defaults=[
ConstantNode(
value=ast.unparse(default).strip() if default else TypeHint.NO_DEFAULT
)
for default in sub_node.args.kw_defaults
],
defaults=[
ConstantNode(
value=ast.unparse(default).strip() if default else TypeHint.NO_DEFAULT
)
for default in sub_node.args.defaults
],
return_=self.clear_quotes(ast.unparse(sub_node.returns).strip()) if sub_node.returns else TypeHint.NO_RETURN,
decorators=[ast.unparse(decorator).strip() for decorator in sub_node.decorator_list],
is_async=isinstance(sub_node, ast.AsyncFunctionDef),
src=ast.unparse(sub_node).strip(),
is_classmethod=True
))
# elif isinstance(sub_node, (ast.Assign, ast.AnnAssign)):
# if isinstance(sub_node, ast.Assign):
# class_node.attrs.append(AttrNode(
# name=sub_node.targets[0].id, # type: ignore
# type=TypeHint.NO_TYPEHINT,
# value=ast.unparse(sub_node.value).strip()
# ))
# elif isinstance(sub_node, ast.AnnAssign):
# class_node.attrs.append(AttrNode(
# name=sub_node.target.id,
# type=ast.unparse(sub_node.annotation).strip(),
# value=ast.unparse(sub_node.value).strip() if sub_node.value else TypeHint.NO_DEFAULT
# ))
# else:
# raise ValueError(f"Unsupported node type: {type(sub_node)}")
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
# 仅打印模块级别的函数
if not self._is_module_level_function(node):
continue
self.functions.append(FunctionNode(
name=node.name,
docs=parse(ast.get_docstring(node)) if ast.get_docstring(node) else None,
posonlyargs=[
ArgNode(
name=arg.arg,
type=self.clear_quotes(ast.unparse(arg.annotation).strip()) if arg.annotation else TypeHint.NO_TYPEHINT,
)
for arg in node.args.posonlyargs
],
args=[
ArgNode(
name=arg.arg,
type=self.clear_quotes(ast.unparse(arg.annotation).strip()) if arg.annotation else TypeHint.NO_TYPEHINT,
)
for arg, default in zip(node.args.args, node.args.defaults)
],
kwonlyargs=[
ArgNode(
name=arg.arg,
type=self.clear_quotes(ast.unparse(arg.annotation).strip()) if arg.annotation else TypeHint.NO_TYPEHINT,
)
for arg in node.args.kwonlyargs
],
kw_defaults=[
ConstantNode(
value=ast.unparse(default).strip() if default else TypeHint.NO_DEFAULT
)
for default in node.args.kw_defaults
],
defaults=[
ConstantNode(
value=ast.unparse(default).strip() if default else TypeHint.NO_DEFAULT
)
for default in node.args.defaults
],
return_=self.clear_quotes(ast.unparse(node.returns).strip()) if node.returns else TypeHint.NO_RETURN,
decorators=[ast.unparse(decorator).strip() for decorator in node.decorator_list],
is_async=isinstance(node, ast.AsyncFunctionDef),
src=ast.unparse(node).strip()
))
elif isinstance(node, (ast.Assign, ast.AnnAssign)):
if not self._is_module_level_variable2(node):
continue
else:
pass
lineno = node.lineno
prev_line = self.get_line_content(lineno - 1).strip()
curr_line = self.get_line_content(lineno).strip()
next_line = self.get_line_content(lineno + 1).strip()
# 获取文档字符串,优先检测下行"""
if next_line.startswith('"""'):
docs = next_line[3:-3]
elif prev_line.startswith('"""'):
docs = prev_line[3:-3]
else:
curr_docs = self.match_line_docs(curr_line)
if curr_docs:
docs = curr_docs
else:
docs = None
# if isinstance(node, ast.Assign):
# for target in node.targets:
# if isinstance(target, ast.Name):
# self.variables.append(AssignNode(
# name=target.id,
# value=ast.unparse(node.value).strip(),
# type=ast.unparse(node.annotation).strip() if isinstance(node, ast.AnnAssign) else TypeHint.NO_TYPEHINT
# ))
if isinstance(node, ast.AnnAssign):
self.variables.append(AssignNode(
name=node.target.id,
value=ast.unparse(node.value).strip() if node.value else TypeHint.NO_DEFAULT,
type=ast.unparse(node.annotation).strip(),
docs=docs
))
def _is_module_level_function(self, node: ast.FunctionDef | ast.AsyncFunctionDef):
for parent in ast.walk(self.tree):
if isinstance(parent, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
if node in parent.body:
return False
return True
def _is_module_level_class(self, node: ast.ClassDef):
for parent in ast.walk(self.tree):
if isinstance(parent, ast.ClassDef):
if node in parent.body:
return False
return True
def _is_module_level_variable(self, node: ast.Assign | ast.AnnAssign):
"""在类方法或函数内部的变量不会被记录"""
# for parent in ast.walk(self.tree):
# if isinstance(parent, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
# if node in parent.body:
# return False
# else:
# for sub_node in parent.body:
# if isinstance(sub_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
# if node in sub_node.body:
# return False
# return True
# 递归检查
def _check(_node, _parent):
if isinstance(_parent, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
if _node in _parent.body:
return False
else:
for sub_node in _parent.body:
if isinstance(sub_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
return _check(_node, sub_node)
return True
for parent in ast.walk(self.tree):
if not _check(node, parent):
return False
return True
def _is_module_level_variable2(self, node: ast.Assign | ast.AnnAssign) -> bool:
"""
检查变量是否在模块级别定义。
"""
for parent in ast.walk(self.tree):
if isinstance(parent, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
if node in parent.body:
return False
return True
def __str__(self):
s = ""
for cls in self.classes:
s += f"class {cls.name}:\n"
for func in self.functions:
s += f"def {func.name}:\n"
for var in self.variables:
s += f"{var.name} = {var.value}\n"
return s

314
litedoc/syntax/node.py Normal file
View File

@ -0,0 +1,314 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/28 下午2:14
@Author : snowykami
@Email : snowykami@outlook.com
@File : node.py
@Software: PyCharm
"""
from typing import Literal, Optional
from enum import Enum
from pydantic import BaseModel, Field
from litedoc.docstring.docstring import Docstring
from litedoc.i18n import get_text
class TypeHint:
NO_TYPEHINT = "NO_TYPE_HINT"
NO_DEFAULT = "NO_DEFAULT"
NO_RETURN = "NO_RETURN"
class AssignNode(BaseModel):
"""
AssignNode is a pydantic model that represents an assignment.
Attributes:
name: str
The name of the assignment.
type: str = ""
The type of the assignment.
value: str
The value of the assignment.
"""
name: str
type: str = ""
value: str
docs: Optional[str] = ""
class ArgNode(BaseModel):
"""
ArgNode is a pydantic model that represents an argument.
Attributes:
name: str
The name of the argument.
type: str = ""
The type of the argument.
default: str = ""
The default value of the argument.
"""
name: str
type: str = TypeHint.NO_TYPEHINT
class AttrNode(BaseModel):
"""
AttrNode is a pydantic model that represents an attribute.
Attributes:
name: str
The name of the attribute.
type: str = ""
The type of the attribute.
value: str = ""
The value of the attribute
"""
name: str
type: str = ""
value: str = ""
class ImportNode(BaseModel):
"""
ImportNode is a pydantic model that represents an import statement.
Attributes:
name: str
The name of the import statement.
as_: str = ""
The alias of the import
"""
name: str
as_: str = ""
class ConstantNode(BaseModel):
"""
ConstantNode is a pydantic model that represents a constant.
Attributes:
value: str
The value of the constant.
"""
value: str
class FunctionNode(BaseModel):
"""
FunctionNode is a pydantic model that represents a function.
Attributes:
name: str
The name of the function.
docs: str = ""
The docstring of the function.
args: list[ArgNode] = []
The arguments of the function.
return_: ReturnNode = None
The return value of the function.
decorators: list[str] = []
The decorators of the function.
is_async: bool = False
Whether the function is asynchronous.
"""
name: str
docs: Optional[Docstring] = None
posonlyargs: list[ArgNode] = []
args: list[ArgNode] = []
kwonlyargs: list[ArgNode] = []
kw_defaults: list[ConstantNode] = []
defaults: list[ConstantNode] = []
return_: str = TypeHint.NO_RETURN
decorators: list[str] = []
src: str
is_async: bool = False
is_classmethod: bool = False
magic_methods: dict[str, str] = {
"__add__" : "+",
"__radd__" : "+",
"__sub__" : "-",
"__rsub__" : "-",
"__mul__" : "*",
"__rmul__" : "*",
"__matmul__" : "@",
"__rmatmul__": "@",
"__mod__" : "%",
"__truediv__": "/",
"__rtruediv__": "/",
"__neg__" : "-",
} # 魔术方法, 例如运算符
def is_private(self):
"""
Check if the function or method is private.
Returns:
bool: True if the function or method is private, False otherwise.
"""
return self.name.startswith("_")
def is_builtin(self):
"""
Check if the function or method is a builtin function or method.
Returns:
bool: True if the function or method is a builtin function or method, False otherwise.
"""
return self.name.startswith("__") and self.name.endswith("__")
def markdown(self, lang: str, indent: int = 0) -> str:
"""
Args:
indent: int
The number of spaces to indent the markdown.
lang: str
The language of the
Returns:
markdown style document
"""
self.complete_default_args()
PREFIX = "" * indent
# if is_classmethod:
# PREFIX = "- #"
func_type = "func" if not self.is_classmethod else "method"
md = ""
# 装饰器部分
if len(self.decorators) > 0:
for decorator in self.decorators:
md += PREFIX + f"### `@{decorator}`\n"
if self.is_async:
md += PREFIX + f"### *async {func_type}* "
else:
md += PREFIX + f"### *{func_type}* "
# code start
# 配对位置参数和位置参数默认值无默认值用TypeHint.NO_DEFAULT
args: list[str] = [] # 可直接", ".join(args)得到位置参数部分
arg_i = 0
if len(self.posonlyargs) > 0:
for arg in self.posonlyargs:
arg_text = f"{arg.name}"
if arg.type != TypeHint.NO_TYPEHINT:
arg_text += f": {arg.type}"
arg_default = self.defaults[arg_i].value
if arg_default != TypeHint.NO_DEFAULT:
arg_text += f" = {arg_default}"
args.append(arg_text)
arg_i += 1
# 加位置参数分割符 /
args.append("/")
for arg in self.args:
arg_text = f"{arg.name}"
if arg.type != TypeHint.NO_TYPEHINT:
arg_text += f": {arg.type}"
arg_default = self.defaults[arg_i].value
if arg_default != TypeHint.NO_DEFAULT:
arg_text += f" = {arg_default}"
args.append(arg_text)
arg_i += 1
if len(self.kwonlyargs) > 0:
# 加关键字参数分割符 *
args.append("*")
for arg, kw_default in zip(self.kwonlyargs, self.kw_defaults):
arg_text = f"{arg.name}"
if arg.type != TypeHint.NO_TYPEHINT:
arg_text += f": {arg.type}"
if kw_default.value != TypeHint.NO_DEFAULT:
arg_text += f" = {kw_default.value}"
args.append(arg_text)
"""魔法方法"""
if self.name in self.magic_methods:
if len(args) == 2:
md += f"`{args[0]} {self.magic_methods[self.name]} {args[1]}"
elif len(args) == 1:
md += f"`{self.magic_methods[self.name]} {args[0]}"
if self.return_ != TypeHint.NO_RETURN:
md += f" => {self.return_}"
else:
md += f"`{self.name}(" # code start
md += ", ".join(args) + ")"
if self.return_ != TypeHint.NO_RETURN:
md += f" -> {self.return_}"
md += "`\n\n" # code end
"""此处预留docstring"""
if self.docs is not None:
md += f"\n{self.docs.markdown(lang, indent)}\n"
else:
pass
# 源码展示
md += PREFIX + f"\n<details>\n<summary> <b>{get_text(lang, 'src')}</b> </summary>\n\n```python\n{self.src}\n```\n</details>\n\n"
return md
def complete_default_args(self):
"""
补全位置参数默认值,用无默认值插入
Returns:
"""
num = len(self.args) + len(self.posonlyargs) - len(self.defaults)
self.defaults = [ConstantNode(value=TypeHint.NO_DEFAULT) for _ in range(num)] + self.defaults
def __str__(self):
return f"def {self.name}({', '.join([f'{arg.name}: {arg.type} = {arg.default}' for arg in self.args])}) -> {self.return_}"
class ClassNode(BaseModel):
"""
ClassNode is a pydantic model that represents a class.
Attributes:
name: str
The name of the class.
docs: str = ""
The docstring of the class.
attrs: list[AttrNode] = []
The attributes of the class.
methods: list[MethodNode] = []
The methods of the class.
inherits: list["ClassNode"] = []
The classes that the class inherits from
"""
name: str
docs: Optional[Docstring] = None
attrs: list[AttrNode] = []
methods: list[FunctionNode] = []
inherits: list[str] = []
def markdown(self, lang: str) -> str:
"""
返回类的markdown文档
Args:
lang: str
The language of the
Returns:
markdown style document
"""
hidden_methods = [
"__str__",
"__repr__",
]
md = ""
md += f"### **class** `{self.name}"
if len(self.inherits) > 0:
md += f"({', '.join([cls for cls in self.inherits])})"
md += "`\n"
for method in self.methods:
if method.name in hidden_methods:
continue
md += method.markdown(lang, 2)
for attr in self.attrs:
if attr.type == TypeHint.NO_TYPEHINT:
md += f"#### ***attr*** `{attr.name} = {attr.value}`\n\n"
else:
md += f"#### ***attr*** `{attr.name}: {attr.type} = {attr.value}`\n\n"
return md