From 4872ca2e8a6afab1be50d7eadad87e560691b546 Mon Sep 17 00:00:00 2001 From: BigOrangeQWQ <2284086963@qq.com> Date: Sun, 8 Sep 2024 23:22:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=20render.jinja=20?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E5=86=85=E5=AE=B9=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/docker_plugin_test.py | 32 ++++-- src/plugins/publish/__init__.py | 6 +- src/plugins/publish/render.py | 7 +- .../publish/templates/render_data.md.jinja | 24 ++-- .../publish/templates/render_error.md.jinja | 103 ++++++++++-------- src/plugins/publish/utils.py | 2 +- src/plugins/publish/validation.py | 20 ++-- src/utils/store_test/store.py | 1 + src/utils/validation/constants.py | 2 +- src/utils/validation/models.py | 3 +- 10 files changed, 119 insertions(+), 81 deletions(-) diff --git a/docker/docker_plugin_test.py b/docker/docker_plugin_test.py index fad12684..3cd6747d 100644 --- a/docker/docker_plugin_test.py +++ b/docker/docker_plugin_test.py @@ -251,8 +251,10 @@ async def run(self): # 创建插件测试项目 await self.create_poetry_project() if self._create: - await self.show_package_info() - await self.show_plugin_dependencies() + await asyncio.gather( + self.show_package_info(), + self.show_plugin_dependencies(), + ) await self.run_poetry_project() metadata = {} @@ -278,8 +280,14 @@ async def run(self): return self._run, self._lines_output async def command(self, cmd: str, timeout: int = 300) -> tuple[bool, str, str]: - """ - 执行命令 + """执行命令 + + Args: + cmd (str): 命令 + timeout (int, optional): 超时限制. Defaults to 300. + + Returns: + tuple[bool, str, str]: 命令执行返回值,标准输出,标准错误 """ try: proc = await create_subprocess_shell( @@ -299,6 +307,7 @@ async def command(self, cmd: str, timeout: int = 300) -> tuple[bool, str, str]: return not code, stdout.decode(), stderr.decode() async def create_poetry_project(self): + """创建 poetry 项目用来测试插件""" if not self.path.exists(): self.path.mkdir() @@ -321,6 +330,7 @@ async def create_poetry_project(self): self._create = True async def show_package_info(self) -> None: + """获取插件的版本与插件信息""" if self.path.exists(): code, stdout, stderr = await self.command( f"poetry show {self.project_link}" @@ -337,6 +347,7 @@ async def show_package_info(self) -> None: self._log_output(f"插件 {self.project_link} 信息获取失败。") async def run_poetry_project(self) -> None: + """运行插件""" if self.path.exists(): # 默认使用 fake 驱动 with open(self.path / ".env", "w", encoding="utf8") as f: @@ -377,6 +388,7 @@ async def run_poetry_project(self) -> None: self._log_output(f" {i}") async def show_plugin_dependencies(self) -> None: + """获取插件的依赖""" if self.path.exists(): code, stdout, stderr = await self.command("poetry export --without-hashes") @@ -414,9 +426,15 @@ def _get_plugin_module_name(self, require: str) -> str | None: def main(): - plugin = PluginTest( - os.environ.get("PLUGIN_INFO", ""), os.environ.get("PLUGIN_CONFIG", None) - ) + """ + 根据传入的环境变量 PLUGIN_INFO 和 PLUGIN_CONFIG 进行测试 + + PLUGIN_INFO 即为该插件的键 + """ + + plugin_info = os.environ.get("PLUGIN_INFO", "") + plugin_config = os.environ.get("PLUGIN_CONFIG", None) + plugin = PluginTest(plugin_info, plugin_config) asyncio.run(plugin.run()) diff --git a/src/plugins/publish/__init__.py b/src/plugins/publish/__init__.py index 3c549bc8..f7fd4154 100644 --- a/src/plugins/publish/__init__.py +++ b/src/plugins/publish/__init__.py @@ -27,8 +27,6 @@ from .utils import ( process_pr_and_issue_title, comment_issue, - commit_and_push, - create_pull_request, ensure_issue_content, ensure_issue_test_button, resolve_conflict_pull_requests, @@ -36,7 +34,6 @@ should_skip_plugin_publish, should_skip_plugin_test, trigger_registry_update, - update_file, ) from .validation import ( validate_adapter_info_from_issue, @@ -206,6 +203,7 @@ async def handle_publish_plugin_check( # 分支命名示例 publish/issue123 branch_name = f"{BRANCH_NAME_PREFIX}{issue_number}" + # 验证之后创建拉取请求和修改议题的标题 await process_pr_and_issue_title( bot, repo_info, result, branch_name, issue_number, title, issue ) @@ -251,6 +249,7 @@ async def handle_adapter_publish_check( # 分支命名示例 publish/issue123 branch_name = f"{BRANCH_NAME_PREFIX}{issue_number}" + # 验证之后创建拉取请求和修改议题的标题 await process_pr_and_issue_title( bot, repo_info, result, branch_name, issue_number, title, issue ) @@ -292,6 +291,7 @@ async def handle_bot_publish_check( # 分支命名示例 publish/issue123 branch_name = f"{BRANCH_NAME_PREFIX}{issue_number}" + # 验证之后创建拉取请求和修改议题的标题 await process_pr_and_issue_title( bot, repo_info, result, branch_name, issue_number, title, issue ) diff --git a/src/plugins/publish/render.py b/src/plugins/publish/render.py index 8857fbc7..0ca22bb7 100644 --- a/src/plugins/publish/render.py +++ b/src/plugins/publish/render.py @@ -1,5 +1,4 @@ from pathlib import Path -from typing import TYPE_CHECKING import jinja2 @@ -7,9 +6,7 @@ from .config import plugin_config from .constants import LOC_NAME_MAP - -if TYPE_CHECKING: - from src.utils.validation import ValidationDict +from src.utils.validation import ValidationDict def tags_to_str(tags: list[dict[str, str]]) -> str: @@ -50,7 +47,7 @@ def loc_to_name(loc: list[str | int]) -> str: env.filters["loc_to_name"] = loc_to_name -async def render_comment(result: "ValidationDict", reuse: bool = False) -> str: +async def render_comment(result: ValidationDict, reuse: bool = False) -> str: """将验证结果转换为评论内容""" title = f"{result.type}: {result.name}" diff --git a/src/plugins/publish/templates/render_data.md.jinja b/src/plugins/publish/templates/render_data.md.jinja index 3682f4ba..b035e12d 100644 --- a/src/plugins/publish/templates/render_data.md.jinja +++ b/src/plugins/publish/templates/render_data.md.jinja @@ -1,23 +1,23 @@ {% macro render_data(key, value, skip_plugin_test) %} {% if key == "homepage" %} -项目 主页 返回状态码 200。 + 项目 主页 返回状态码 200。 {%- elif key == "tags" and value %} -标签: {{ value|tags_to_str }}。 + 标签: {{ value|tags_to_str }}。 {%- elif key == "project_link" %} -项目 {{ value }} 已发布至 PyPI。 + 项目 {{ value }} 已发布至 PyPI。 {%- elif key == "type" %} -插件类型: {{ value }}。 + 插件类型: {{ value }}。 {%- elif key == "load" %} -插件加载测试: {{ value }}。 + 插件加载测试: {{ value }}。 {%- elif key == "supported_adapters" %} -插件支持的适配器: {{ value|supported_adapters_to_str }}。 + 插件支持的适配器: {{ value|supported_adapters_to_str }}。 {%- elif key == "action_url" %} -{% if skip_plugin_test %} -插件 加载测试 已跳过。 -{%- else %} -插件 加载测试 通过。 -{%- endif %} + {% if skip_plugin_test %} + 插件 加载测试 已跳过。 + {%- else %} + 插件 加载测试 通过。 + {%- endif %} {% else %} -{{ key }}: {{ value }}。 + {{ key }}: {{ value }}。 {%- endif %} {% endmacro %} diff --git a/src/plugins/publish/templates/render_error.md.jinja b/src/plugins/publish/templates/render_error.md.jinja index f6f96cd6..581572d4 100644 --- a/src/plugins/publish/templates/render_error.md.jinja +++ b/src/plugins/publish/templates/render_error.md.jinja @@ -1,47 +1,64 @@ {% macro render_error(error) %} + {% set loc = error.loc %} {% set type = error.type %} -{%- if loc|length == 3 and loc[0] == "tags" and type == "missing" %} -第 {{ loc[1] + 1 }} 个标签缺少 {{ loc[2] }} 字段。
请确保标签字段完整。
-{% elif loc|length == 3 and loc[0] == "tags" and type == "string_too_long" %} -第 {{ loc[1] + 1 }} 个标签名称过长
请确保标签名称不超过 10 个字符。
-{%- elif loc|length == 3 and loc[0] == "tags" and type == "color_error" %} -第 {{ loc[1] + 1 }} 个标签颜色错误
请确保标签颜色符合十六进制颜色码规则。
-{%- elif loc|length == 2 and loc[0] == "tags" and type == "model_type" %} -第 {{ loc[1] + 1 }} 个标签格式错误。
请确保标签为字典。
-{%- elif type == "homepage" and error.ctx.status_code != -1 %} -项目 主页 返回状态码 {{ error.ctx.status_code }}。
请确保你的项目主页可访问。
-{%- elif type == "homepage" and error.ctx.status_code == -1 %} -项目 主页 访问出错。
错误信息{{ error.ctx.msg }}
-{%- elif type == "project_link.not_found" %} -项目 {{ error.input }} 未发布至 PyPI。
请将你的项目发布至 PyPI。
-{%- elif type == "project_link.name" %} -PyPI 项目名 {{ error.input }} 不符合规范。
请确保项目名正确。
-{%- elif type == "module_name" %} -包名 {{ error.input }} 不符合规范。
请确保包名正确。
-{%- elif type == "duplication" %} -{{ error.msg }}
请确保没有重复发布。
-{%- elif type == "plugin_test" %} -插件加载测试未通过。
测试输出{{ error.ctx.output }}
-{%- elif type == "metadata" %} -无法获取到插件元数据。
{{ "请填写插件元数据" if error.ctx.plugin_test_result else "请确保插件正常加载" }}。
-{%- elif type == "plugin.type" %} -插件类型 {{ error.input }} 不符合规范。
请确保插件类型正确,当前仅支持 application 与 library。
-{%- elif type == "supported_adapters.missing" %} -适配器 {{ ', '.join(error.ctx.missing_adapters) }} 不存在。
请确保适配器模块名称正确。
-{%- elif type == "missing" %} -{{ loc|loc_to_name }}: 无法匹配到数据。
请确保填写该项目。
-{%- elif type == "model_type" %} -{{ loc|loc_to_name }}: 格式错误。
请确保其为字典。
-{%- elif type == "list_type" %} -{{ loc|loc_to_name }}: 格式错误。
请确保其为列表。
-{%- elif type == "set_type" %} -{{ loc|loc_to_name }}: 格式错误。
请确保其为集合。
-{%- elif type == "json_type" %} -{{ loc|loc_to_name }}: 解码失败。
请确保其为 JSON 格式。
-{%- elif type == "string_too_long" %} -{{ loc|loc_to_name }}: 字符过多。
请确保其不超过 {{ error.ctx.max_length }} 个字符。
-{%- else %} -{{ error.loc|loc_to_name }}: {{ error.msg }} -{%- endif %} +{% set ctx = error.ctx %} +{% set name = loc|loc_to_name %} +{% set input = error.input %} + +{% macro render_tag_error(index, message, dt_message) %} +第 {{ index + 1 }} 个标签{{ message }}。
请确保标签{{ dt_message }}。
{% endmacro %} + +{% macro detail_message(summary, detail) %} +
{{ summary }}{{ detail }}
+{% endmacro %} + +{% if loc|length == 3 and loc[0] == "tags" %} + {% if type == "missing" %} + {{ render_tag_error(loc[1], "缺少第" ~ loc[2] ~ " 字段", "请确保标签字段完整。") }} + {% elif type == "string_too_long" %} + {{ render_tag_error(loc[1], "名称过长", "请确保标签名称不超过 10 个字符。") }} + {% elif type == "color_error" %} + {{ render_tag_error(loc[1], "颜色错误", "请确保标签颜色符合十六进制颜色码规则。") }} + {% endif %} +{% elif loc|length == 2 and loc[0] == "tags" and type == "model_type" %} + {{ render_tag_error(loc[1], "格式错误", "请确保标签为字典。") }} +{% elif type == "homepage" %} + {% if ctx.status_code != -1 %} + 项目 主页 返回状态码 {{ ctx.status_code }}。
请确保你的项目主页可访问。
+ {% else %} + 项目 主页 访问出错。{{ detail_message("错误信息", ctx.error) }} + {% endif %} +{% elif type == "project_link.not_found" %} + 项目 {{ input }} 未发布至 PyPI。
请将你的项目发布至 PyPI。
+{% elif type == "project_link.name" %} + PyPI 项目名 {{ input }} 不符合规范。
请确保项目名正确。
+{% elif type == "module_name" %} + 包名 {{ input }} 不符合规范。
请确保包名正确。
+{% elif type == "duplication" %} + {{ error.msg }}
请确保没有重复发布。
+{% elif type == "plugin.test" %} + 插件加载测试未通过。{{ detail_message("测试输出", ctx.output) }} +{% elif type == "metadata" %} + 无法获取到插件元数据。
{{ "请填写插件元数据" if ctx.plugin_test_result else "请确保插件正常加载" }}。
+{% elif type == "plugin.type" %} + 插件类型 {{ input }} 不符合规范。
请确保插件类型正确,当前仅支持 application 与 library。
+{% elif type == "supported_adapters.missing" %} + 适配器 {{ ', '.join(ctx.missing_adapters) }} 不存在。
请确保适配器模块名称正确。
+{% elif type == "missing" %} + {{ name }}: 无法匹配到数据。
请确保填写该项目。
+{% elif type == "model_type" %} + {{ name }}: 格式错误。
请确保其为字典。
+{% elif type == "list_type" %} + {{ name }}: 格式错误。
请确保其为列表。
+{% elif type == "set_type" %} + {{ name }}: 格式错误。
请确保其为集合。
+{% elif type == "json_type" %} + {{ name }}: 解码失败。
请确保其为 JSON 格式。
+{% elif type == "string_too_long" %} + {{ name }}: 字符过多。
请确保其不超过 {{ ctx.max_length }} 个字符。
+{% else %} + {{ error.name }}: {{ error.msg }} +{% endif %} +{% endmacro %} \ No newline at end of file diff --git a/src/plugins/publish/utils.py b/src/plugins/publish/utils.py index 7c580072..f7bfaa55 100644 --- a/src/plugins/publish/utils.py +++ b/src/plugins/publish/utils.py @@ -510,7 +510,7 @@ async def trigger_registry_update( client_payload = { "type": publish_type.value, - "key": f"{result.data['project_link']}:{result.data['module_name']}", + "key": f"{result.data["project_link"]}:{result.data["module_name"]}", "config": config.group(1) if config else "", "data": json.dumps(result.data), } diff --git a/src/plugins/publish/validation.py b/src/plugins/publish/validation.py index 32ab1aee..381f30f7 100644 --- a/src/plugins/publish/validation.py +++ b/src/plugins/publish/validation.py @@ -46,11 +46,12 @@ def strip_ansi(text: str | None) -> str: async def validate_plugin_info_from_issue(issue: "Issue") -> ValidationDict: - """从议题中提取插件信息""" + """从议题中获取插件信息,并且运行插件测试加载且获取插件元信息后进行验证""" body = issue.body if issue.body else "" author = issue.user.id if issue.user else "" author_id = issue.user.id if issue.user else None + # 从议题里提取插件所需信息 raw_data: dict[str, Any] = extract_publish_info_from_issue( { "module_name": PLUGIN_MODULE_NAME_PATTERN, @@ -67,6 +68,10 @@ async def validate_plugin_info_from_issue(issue: "Issue") -> ValidationDict: with plugin_config.input_config.plugin_path.open("r", encoding="utf-8") as f: previous_data: list[dict[str, str]] = json.load(f) + raw_data["author"] = author + raw_data["author_id"] = author_id + + # 运行插件测试,获取插件测试输出与元信息 plugin_test_result: DockerTestResult = await DockerPluginTest( DOCKER_IMAGES, project_link, module_name, test_config ).run("3.10") @@ -82,11 +87,10 @@ async def validate_plugin_info_from_issue(issue: "Issue") -> ValidationDict: "output": plugin_test_output, "metadata": plugin_test_metadata, "previous_data": previous_data, - "author": author, - "author_id": author_id, } ) - # 如果插件测试被跳过,则从议题中获取信息 + + # 如果插件被跳过,则从议题获取插件信息 if plugin_config.skip_plugin_test: plugin_info = extract_publish_info_from_issue( { @@ -100,23 +104,23 @@ async def validate_plugin_info_from_issue(issue: "Issue") -> ValidationDict: ) raw_data.update(plugin_info) elif plugin_test_metadata: - raw_data.update(plugin_test_metadata) - raw_data["desc"] = raw_data.get("description") + # 发布信息更新插件元数据 + raw_data.update(plugin_test_metadata.model_dump()) else: # 插件缺少元数据 # 可能为插件测试未通过,或者插件未按规范编写 raw_data["name"] = project_link - # 如果升级至 pydantic 2 后,可以使用 validation-context + # 传入的验证插件信息的上下文 validation_context = { "previous_data": raw_data.get("previous_data"), "skip_plugin_test": raw_data.get("skip_plugin_test"), "plugin_test_output": raw_data.get("plugin_test_output"), } + # 验证插件相关信息 validate_data = validate_info(PublishType.PLUGIN, raw_data, validation_context) - # 如果是插件,还需要额外验证插件加载测试结果 if ( validate_data.data.get("metadata") is None and not plugin_config.skip_plugin_test diff --git a/src/utils/store_test/store.py b/src/utils/store_test/store.py index 9e016011..a7a5138a 100644 --- a/src/utils/store_test/store.py +++ b/src/utils/store_test/store.py @@ -194,6 +194,7 @@ async def merge_data( dump_json(DRIVERS_PATH, self._store_drivers) dump_json(PLUGINS_PATH, list(plugins.values())) dump_json(RESULTS_PATH, results) + dump_json(PLUGIN_CONFIG_PATH, self._plugin_configs) async def run(self, limit: int, force: bool = False): """运行商店测试 diff --git a/src/utils/validation/constants.py b/src/utils/validation/constants.py index bf1de29f..c3fefeee 100644 --- a/src/utils/validation/constants.py +++ b/src/utils/validation/constants.py @@ -1,5 +1,5 @@ import re -from typing_extensions import LiteralString +from typing import LiteralString # PyPI 格式 PYPI_PACKAGE_NAME_PATTERN = re.compile( diff --git a/src/utils/validation/models.py b/src/utils/validation/models.py index 45099cdc..3d10f142 100644 --- a/src/utils/validation/models.py +++ b/src/utils/validation/models.py @@ -241,7 +241,8 @@ def plugin_test_metadata_validator( context = info.context if context is None: raise PydanticCustomError("validation_context", "未获取到验证上下文") - + if v is None: + raise PydanticCustomError("plugin.metadata", "插件缺少元数据") if context.get("skip_plugin_test"): return None return v