forked from bot/app
⬇️ 更新文档样式
This commit is contained in:
parent
50c0216435
commit
be5a4b270d
3
.github/workflows/deploy-docs.yml
vendored
3
.github/workflows/deploy-docs.yml
vendored
@ -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
|
||||||
|
|
||||||
|
@ -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
|
|
||||||
"""
|
|
@ -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()
|
|
@ -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
|
|
||||||
"""
|
|
@ -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
|
|
@ -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}")
|
|
131
litedoc/i18n.py
131
litedoc/i18n.py
@ -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
|
|
@ -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)
|
|
@ -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
|
|
||||||
"""
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
|
Loading…
Reference in New Issue
Block a user