⬇️ 更新文档样式

This commit is contained in:
远野千束 2024-08-29 15:50:26 +08:00
parent 50c0216435
commit be5a4b270d
13 changed files with 1 additions and 1397 deletions

View File

@ -20,7 +20,6 @@ jobs:
fetch-depth: 0 fetch-depth: 0
# 如果你文档需要 Git 子模块,取消注释下一行 # 如果你文档需要 Git 子模块,取消注释下一行
# submodules: true # submodules: true
- name: 安装 pnpm - name: 安装 pnpm
uses: pnpm/action-setup@v2 uses: pnpm/action-setup@v2
with: with:
@ -41,7 +40,7 @@ jobs:
- name: 生成API markdown - name: 生成API markdown
run: |- run: |-
python -m pip install pydantic python -m pip install litedoc
python -m litedoc liteyuki -o docs/dev/api -l zh-Hans -t vuepress python -m litedoc liteyuki -o docs/dev/api -l zh-Hans -t vuepress
python -m litedoc liteyuki -o docs/en/dev/api -l en -t vuepress python -m litedoc liteyuki -o docs/en/dev/api -l en -t vuepress

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/28 下午12:52
@Author : snowykami
@Email : snowykami@outlook.com
@File : __init__.py.py
@Software: PyCharm
"""

View File

@ -1,48 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/28 下午4:08
@Author : snowykami
@Email : snowykami@outlook.com
@File : __main__.py
@Software: PyCharm
"""
# command line tool
# args[0] path
# -o|--output output path
# -l|--lang zh-Hans en jp default zh-Hans
import argparse
import os
import sys
from litedoc.output import generate_from_module
def main():
parser = argparse.ArgumentParser(description="Generate documentation from Python modules.")
parser.add_argument("path", type=str, help="Path to the Python module or package.")
parser.add_argument("-o", "--output", default="doc-output", type=str, help="Output directory.")
parser.add_argument("-c", "--contain-top", action="store_true", help="Whether to contain top-level dir in output dir.")
parser.add_argument("-l", "--lang", default="zh_Hans", type=str, help="Languages of the document.")
parser.add_argument("-t", "--theme", default="vitepress", type=str, help="Theme of the document.")
parser.add_argument("-s", "--style", default="google", type=str, help="Style of the document.")
args = parser.parse_args()
if not os.path.exists(args.path):
print(f"Error: The path {args.path} does not exist.")
sys.exit(1)
if not os.path.exists(args.output):
os.makedirs(args.output)
lang = args.lang
generate_from_module(args.path, args.output, with_top=args.contain_top, lang=lang, theme=args.theme, style=args.style)
if __name__ == '__main__':
main()

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/28 下午1:46
@Author : snowykami
@Email : snowykami@outlook.com
@File : __init__.py.py
@Software: PyCharm
"""

View File

@ -1,157 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/28 下午1:46
@Author : snowykami
@Email : snowykami@outlook.com
@File : docstring.py
@Software: PyCharm
"""
from typing import Optional
from pydantic import BaseModel, Field
from litedoc.i18n import get_text
class Attr(BaseModel):
name: str
type: str = ""
desc: str = ""
class Args(BaseModel):
name: str
type: str = ""
desc: str = ""
class Return(BaseModel):
desc: str = ""
class Exception_(BaseModel):
name: str
desc: str = ""
class Raise(BaseModel):
exceptions: list[Exception_] = []
class Example(BaseModel):
desc: str = ""
input: str = ""
output: str = ""
class Docstring(BaseModel):
desc: str = ""
args: list[Args] = []
attrs: list[Attr] = []
return_: Optional[Return] = None
raise_: list[Exception_] = []
example: list[Example] = []
def add_desc(self, desc: str):
if self.desc == "":
self.desc = desc
else:
self.desc += "\n" + desc
def add_arg(self, name: str, type_: str = "", desc: str = ""):
self.args.append(Args(name=name, type=type_, desc=desc))
def add_attrs(self, name: str, type_: str = "", desc: str = ""):
self.attrs.append(Attr(name=name, type=type_, desc=desc))
def add_return(self, desc: str = ""):
self.return_ = Return(desc=desc)
def add_raise(self, name: str, desc: str = ""):
self.raise_.append(Exception_(name=name, desc=desc))
def add_example(self, desc: str = "", input_: str = "", output: str = ""):
self.example.append(Example(desc=desc, input=input_, output=output))
def reduction(self) -> str:
"""
通过解析结果还原docstring
Args:
Returns:
"""
ret = ""
ret += self.desc + "\n"
if self.args:
ret += "Args:\n"
for arg in self.args:
ret += f" {arg.name}: {arg.type}\n {arg.desc}\n"
if self.attrs:
ret += "Attributes:\n"
for attr in self.attrs:
ret += f" {attr.name}: {attr.type}\n {attr.desc}\n"
if self.return_:
ret += "Returns:\n"
ret += f" {self.return_.desc}\n"
if self.raise_:
ret += "Raises:\n"
for exception in self.raise_:
ret += f" {exception.name}\n {exception.desc}\n"
if self.example:
ret += "Examples:\n"
for example in self.example:
ret += f" {example.desc}\n Input: {example.input}\n Output: {example.output}\n"
return ret
def markdown(self, lang: str, indent: int = 4, is_classmethod: bool = False) -> str:
"""
生成markdown文档
Args:
is_classmethod:
lang:
indent:
Returns:
"""
PREFIX = "" * indent
ret = ""
# ret += self.desc + "\n\n"
# print(self.reduction())
# print(self.desc, self.return_)
# 单数属性
if self.desc:
ret += PREFIX + f"\n**{get_text(lang, 'desc')}**: {self.desc}\n"
# 复数属性
if self.args:
ret += PREFIX + f"\n**{get_text(lang, 'docstring.args')}**:\n"
for arg in self.args:
ret += PREFIX + f"> - {arg.name}: {arg.type} {arg.desc}\n"
if self.attrs:
ret += PREFIX + f"\n**{get_text(lang, 'docstring.attrs')}**:\n"
for attr in self.attrs:
ret += PREFIX + f"> - {attr.name}: {attr.type} {attr.desc}\n"
# 单数属性
if self.return_ is not None:
ret += PREFIX + f"\n**{get_text(lang, 'docstring.return')}**: {self.return_.desc}\n"
# 复数属性
if self.raise_:
ret += PREFIX + f"\n**{get_text(lang, 'docstring.raises')}**:\n"
for exception in self.raise_:
ret += PREFIX + f"> - {exception.name} {exception.desc}\n"
if self.example:
ret += PREFIX + f"\n**{get_text(lang, 'docstring.example')}**:\n"
for example in self.example:
ret += PREFIX + f" - {example.desc}\n> **{get_text(lang, 'docs.input')}**: {example.input}\n> **{get_text(lang, 'docs.output')}**: {example.output}\n"
return ret
def __str__(self):
return self.desc

View File

@ -1,192 +0,0 @@
"""
Google docstring parser for Python.
"""
from typing import Optional
from litedoc.docstring.docstring import Docstring
class Parser:
...
class GoogleDocstringParser(Parser):
_tokens = {
"Args" : "args",
"Arguments" : "args",
"参数" : "args",
"Return" : "return",
"Returns" : "return",
"返回" : "return",
"Attribute" : "attribute",
"Attributes" : "attribute",
"属性" : "attribute",
"Raises" : "raises",
"Raise" : "raises",
"引发" : "raises",
"Example" : "example",
"Examples" : "example",
"示例" : "example",
"Yields" : "yields",
"Yield" : "yields",
"产出" : "yields",
"Requires" : "requires",
"Require" : "requires",
"需要" : "requires",
"FrontMatter": "front_matter",
"前言" : "front_matter",
}
def __init__(self, docstring: str, indent: int = 4):
self.lines = docstring.splitlines()
self.indent = indent
self.lineno = 0 # Current line number
self.char = 0 # Current character position
self.docstring = Docstring()
def read_line(self, move: bool = True) -> str:
"""
每次读取一行
Args:
move: 是否移动指针
Returns:
"""
if self.lineno >= len(self.lines):
return ""
line = self.lines[self.lineno]
if move:
self.lineno += 1
return line
def match_token(self) -> Optional[str]:
"""
解析下一行的token
Returns:
"""
for token in self._tokens:
line = self.read_line(move=False)
if line.strip().startswith(token):
self.lineno += 1
return self._tokens[token]
return None
def parse_args(self):
"""
依次解析后面的参数行直到缩进小于等于当前行的缩进
"""
while line := self.match_next_line():
if ":" in line:
name, desc = line.split(":", 1)
self.docstring.add_arg(name.strip(), desc.strip())
else:
self.docstring.add_arg(line.strip())
def parse_return(self):
"""
解析返回值行
"""
if line := self.match_next_line():
self.docstring.add_return(line.strip())
def parse_raises(self):
"""
解析异常行
"""
while line := self.match_next_line():
if ":" in line:
name, desc = line.split(":", 1)
self.docstring.add_raise(name.strip(), desc.strip())
else:
self.docstring.add_raise(line.strip())
def parse_example(self):
"""
解析示例行
"""
while line := self.match_next_line():
if ":" in line:
name, desc = line.split(":", 1)
self.docstring.add_example(name.strip(), desc.strip())
else:
self.docstring.add_example(line.strip())
def parse_attrs(self):
"""
解析属性行
"""
while line := self.match_next_line():
if ":" in line:
name, desc = line.split(":", 1)
self.docstring.add_attrs(name.strip(), desc.strip())
else:
self.docstring.add_attrs(line.strip())
def match_next_line(self) -> Optional[str]:
"""
在一个子解析器中解析下一行直到缩进小于等于当前行的缩进
Returns:
"""
line = self.read_line(move=False)
if line.startswith(" " * self.indent):
self.lineno += 1
return line[self.indent:]
else:
return None
def parse(self) -> Docstring:
"""
逐行解析直到遇到EOS
最开始未解析到的内容全部加入desc
Returns:
"""
add_desc = True
while self.lineno < len(self.lines):
token = self.match_token()
if token is None and add_desc:
self.docstring.add_desc(self.lines[self.lineno].strip())
if token is not None:
add_desc = False
match token:
case "args":
self.parse_args()
case "return":
self.parse_return()
case "attribute":
self.parse_attrs()
case "raises":
self.parse_raises()
case "example":
self.parse_example()
case _:
self.lineno += 1
return self.docstring
class NumpyDocstringParser(Parser):
...
class ReStructuredParser(Parser):
...
def parse(docstring: str, parser: str = "google", indent: int = 4) -> Docstring:
if parser == "google":
return GoogleDocstringParser(docstring, indent).parse()
else:
raise ValueError(f"Unknown parser: {parser}")

View File

@ -1,131 +0,0 @@
# -*- coding: utf-8 -*-
"""
Internationalization module.
"""
from typing import Optional, TypeAlias
NestedDict: TypeAlias = dict[str, 'str | NestedDict']
i18n_dict: dict[str, NestedDict] = {
"en" : {
"docstring": {
"args" : "Arguments",
"return" : "Return",
"attribute": "Attribute",
"raises" : "Raises",
"example" : "Examples",
"yields" : "Yields",
},
"src": "Source code",
"desc": "Description",
"type": "Type",
},
"zh-Hans": {
"docstring": {
"args" : "参数",
"return" : "返回",
"attribute": "属性",
"raises" : "引发",
"example" : "示例",
"yields" : "产出",
},
"src": "源代码",
"desc": "说明",
"type": "类型",
},
"zh-Hant": {
"docstring": {
"args" : "變數説明",
"return" : "返回",
"attribute": "屬性",
"raises" : "抛出",
"example" : "範例",
"yields" : "產出",
},
"src": "源碼",
"desc": "説明",
"type": "類型",
},
"ja" : {
"docstring": {
"args" : "引数",
"return" : "戻り値",
"attribute": "属性",
"raises" : "例外",
"example" : "",
"yields" : "生成",
},
"src": "ソースコード",
"desc": "説明",
"type": "タイプ",
},
}
def flat_i18n_dict(data: dict[str, NestedDict]) -> dict[str, dict[str, str]]:
"""
Flatten i18n_dict.
Examples:
```python
{
"en": {
"docs": {
"key1": "val1",
"key2": "val2",
}
}
}
```
to
```python
{
"en": {
"docs.key1": "val1",
"docs.key2": "val2",
}
}
```
Returns:
"""
ret: dict[str, dict[str, str]] = {}
def _flat(_lang_data: NestedDict) -> dict[str, str]:
res = {}
for k, v in _lang_data.items():
if isinstance(v, dict):
for kk, vv in _flat(v).items():
res[f"{k}.{kk}"] = vv
else:
res[k] = v
return res
for lang, lang_data in data.items():
ret[lang] = _flat(lang_data)
return ret
i18n_flat_dict = flat_i18n_dict(i18n_dict)
def get_text(lang: str, key: str, default: Optional[str] = None, fallback: Optional[str] = "en") -> str:
"""
Get text from i18n_dict.
Args:
lang: language name
key: text key
default: default text, if None return fallback language or key
fallback: fallback language, priority is higher than default
Returns:
str: text
"""
if lang in i18n_flat_dict:
if key in i18n_flat_dict[lang]:
return i18n_flat_dict[lang][key]
if fallback is not None:
return i18n_flat_dict.get(fallback, {}).get(key, default or key)
else:
return default or key

View File

@ -1,116 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/28 下午3:59
@Author : snowykami
@Email : snowykami@outlook.com
@File : output.py
@Software: PyCharm
"""
import os.path
from litedoc.style.markdown import generate
from litedoc.syntax.astparser import AstParser
def write_to_file(content: str, output: str) -> None:
"""
Write content to file.
Args:
content: str, content to write.
output: str, path to output file.
"""
if not os.path.exists(os.path.dirname(output)):
os.makedirs(os.path.dirname(output))
with open(output, "w", encoding="utf-8") as f:
f.write(content)
def get_file_list(module_folder: str):
file_list = []
for root, dirs, files in os.walk(module_folder):
for file in files:
if file.endswith((".py", ".pyi")):
file_list.append(os.path.join(root, file))
return file_list
def get_relative_path(base_path: str, target_path: str) -> str:
"""
获取相对路径
Args:
base_path: 基础路径
target_path: 目标路径
"""
return os.path.relpath(target_path, base_path)
def generate_from_module(module_folder: str,
output_dir: str,
with_top: bool = False,
lang: str = "zh-Hans",
ignored_paths=None,
theme: str = "vitepress",
style: str = "google"
):
"""
生成文档
Args:
module_folder: 模块文件夹
output_dir: 输出文件夹
with_top: 是否包含顶层文件夹 False时例如docs/api/module_a, docs/api/module_b True时例如docs/api/module/module_a.md docs/api/module/module_b.md
ignored_paths: 忽略的路径
lang: 语言
theme: 主题
style: 样式
"""
if ignored_paths is None:
ignored_paths = []
file_data: dict[str, str] = {} # 路径 -> 字串
file_list = get_file_list(module_folder)
# 清理输出目录
if not os.path.exists(output_dir):
os.makedirs(output_dir)
replace_data = {
"__init__": "index" if theme == "vitepress" else "README",
".py" : ".md",
}
for pyfile_path in file_list:
if any(ignored_path.replace("\\", "/") in pyfile_path.replace("\\", "/") for ignored_path in ignored_paths):
continue
no_module_name_pyfile_path = get_relative_path(module_folder, pyfile_path) # 去头路径
# markdown相对路径
rel_md_path = pyfile_path if with_top else no_module_name_pyfile_path
for rk, rv in replace_data.items():
rel_md_path = rel_md_path.replace(rk, rv)
abs_md_path = os.path.join(output_dir, rel_md_path)
# 获取模块信息
ast_parser = AstParser(open(pyfile_path, "r", encoding="utf-8").read())
# 生成markdown
front_matter = {
"title": pyfile_path.replace("\\", "/").
replace("/", ".").
replace(".py", "").
replace(".__init__", ""),
}
md_content = generate(ast_parser, lang=lang, frontmatter=front_matter)
print(f"Generate {pyfile_path} -> {abs_md_path}")
file_data[abs_md_path] = md_content
for fn, content in file_data.items():
write_to_file(content, fn)

View File

@ -1,10 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/28 下午3:39
@Author : snowykami
@Email : snowykami@outlook.com
@File : __init__.py.py
@Software: PyCharm
"""

View File

@ -1,60 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/28 下午3:39
@Author : snowykami
@Email : snowykami@outlook.com
@File : markdown.py
@Software: PyCharm
"""
from typing import Optional
from litedoc.syntax.astparser import AstParser
from litedoc.syntax.node import *
from litedoc.i18n import get_text
def generate(parser: AstParser, lang: str, frontmatter: Optional[dict] = None, style: str = "google") -> str:
"""
Generate markdown style document from ast
You can modify this function to generate markdown style that enjoys you
Args:
parser:
lang: language
frontmatter:
style: style of docs
Returns:
markdown style document
"""
if frontmatter is not None:
md = "---\n"
for k, v in frontmatter.items():
md += f"{k}: {v}\n"
md += "---\n"
else:
md = ""
# var > func > class
"""遍历函数"""
for func in parser.functions:
if func.name.startswith("_"):
continue
md += func.markdown(lang)
"""遍历类"""
for cls in parser.classes:
md += cls.markdown(lang)
"""遍历变量"""
for var in parser.variables:
md += f"### ***var*** `{var.name} = {var.value}`\n\n"
if var.type != TypeHint.NO_TYPEHINT:
md += f"- **{get_text(lang, 'type')}**: `{var.type}`\n\n"
if var.docs is not None:
md += f"- **{get_text(lang, 'desc')}**: {var.docs}\n\n"
return md

View File

@ -1,300 +0,0 @@
# -*- 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, style: str = "google"):
"""
从代码解析AST
Args:
code: 代码
style: 注释风格
"""
self.style = style
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), parser=self.style) 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), parser=self.style) 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), parser=self.style) 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

View File

@ -1,314 +0,0 @@
# -*- 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

View File

@ -1,47 +0,0 @@
# -*- coding: utf-8 -*-
"""
Copyright (C) 2020-2024 LiteyukiStudio. All Rights Reserved
@Time : 2024/8/29 下午12:02
@Author : snowykami
@Email : snowykami@outlook.com
@File : translator.py
@Software: PyCharm
"""
from typing import Optional
from translate import Translator # type: ignore
# 特殊映射语言
i18n_lang2googletrans_lang = {
"zh-Hans": "zh-cn",
"zh-Hant": "zh-tw",
"en" : "en",
}
def get_google_lang(lang: str) -> str:
"""
Get google translate language
Args:
lang: language
Returns:
google translate language
"""
return i18n_lang2googletrans_lang.get(lang, lang)
def translate(text: str, lang: str, source_lang: str) -> str:
"""
Translate text to target language
Args:
source_lang:
text: text
lang: target language
Returns:
translated text
"""
if lang == source_lang:
return text
google_lang = get_google_lang(lang)
return Translator(to_lang=google_lang, from_lang=source_lang).translate(text)