diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..43be232 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,68 @@ +name: Tests + +on: + push: + branches: + - main + - dev + paths-ignore: + - '**.md' + - '.gitignore' + - '.idea/**' + pull_request: + branches: + - main + - dev + paths-ignore: + - '**.md' + - '.gitignore' + - '.idea/**' + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.11"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + + - name: Lint with flake8 + run: | + # 1. 检查严重错误和不安全的导入习惯 + # E9: 语法错误 + # F63: 总是为真的测试 + # F7: 编译错误 + # F82: 未定义的变量 + # F401: 导入但未使用的模块 + # F403: 使用了 'from module import *' + flake8 . --count --select=E9,F63,F7,F82,F401,F403 --show-source --statistics + + # 2. 其他风格检查 (作为警告,不中断构建) + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Run tests + run: | + # 确保当前目录在 PYTHONPATH 中,以便测试脚本可以导入插件包 + export PYTHONPATH=$PYTHONPATH:. + # 运行所有测试并显示详细输出 + pytest -v diff --git a/.gitignore b/.gitignore index 5f3da52..bd767bc 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,14 @@ /dataSources.local.xml /.idea/ /sakura_flow/__pycache__/ + +# Python 缓存 +__pycache__/ +*.py[cod] +*$py.class + +# Pytest 缓存 +.pytest_cache/ + +# MCDR 打包输出 +package/ diff --git a/__main__.py b/__main__.py index 6588081..25d8718 100644 --- a/__main__.py +++ b/__main__.py @@ -1,9 +1,10 @@ import argparse import os -import sys -from sakura_flow.manager import TodoManager -from sakura_flow.controller import TodoController + from sakura_flow.cli_entry import register_cli_commands, handle_cli_command +from sakura_flow.controller import TodoController +from sakura_flow.manager import TodoManager + def main(): parser = argparse.ArgumentParser(description="Sakura Flow CLI") diff --git a/lang/zh_cn.json b/lang/zh_cn.json index f8f8c52..ab64460 100644 --- a/lang/zh_cn.json +++ b/lang/zh_cn.json @@ -1,120 +1,120 @@ { - "todo.common.task": "任务", - "todo.common.creator": "创建者", - "todo.common.tier": "等级", - "todo.common.priority": "优先级", - "todo.common.collaborators": "负责人", - "todo.common.dependencies": "前置依赖", - "todo.common.status": "状态", - "todo.common.description": "描述", - "todo.common.unknown": "未知", - "todo.common.unassigned": "未分配", - "todo.common.none": "无", - "todo.common.usage": "用法", - "todo.common.alias": "缩写", - "todo.common.labels": "任务标签", - "todo.common.title": "标题", + "sakuraflow.common.task": "任务", + "sakuraflow.common.creator": "创建者", + "sakuraflow.common.tier": "等级", + "sakuraflow.common.priority": "优先级", + "sakuraflow.common.collaborators": "负责人", + "sakuraflow.common.dependencies": "前置依赖", + "sakuraflow.common.status": "状态", + "sakuraflow.common.description": "描述", + "sakuraflow.common.unknown": "未知", + "sakuraflow.common.unassigned": "未分配", + "sakuraflow.common.none": "无", + "sakuraflow.common.usage": "用法", + "sakuraflow.common.alias": "缩写", + "sakuraflow.common.labels": "任务标签", + "sakuraflow.common.title": "标题", - "todo.prop.title": "任务标题", - "todo.prop.description": "任务描述", - "todo.prop.status": "任务状态", - "todo.prop.tier": "电压等级", - "todo.prop.priority": "优先级", - "todo.prop.collaborators": "协作人员", - "todo.prop.dependencies": "前置依赖", - "todo.prop.labels": "任务标签", + "sakuraflow.prop.title": "任务标题", + "sakuraflow.prop.description": "任务描述", + "sakuraflow.prop.status": "任务状态", + "sakuraflow.prop.tier": "电压等级", + "sakuraflow.prop.priority": "优先级", + "sakuraflow.prop.collaborators": "协作人员", + "sakuraflow.prop.dependencies": "前置依赖", + "sakuraflow.prop.labels": "任务标签", - "todo.action.restore": "恢复任务", - "todo.action.resume": "恢复", - "todo.action.pause": "暂停", - "todo.action.complete": "完工", - "todo.action.note": "笔记", - "todo.action.claim": "认领", - "todo.action.append": "追加", - "todo.action.modify": "修改", - "todo.action.remove": "移除", - "todo.action.prev_page": "上一页", - "todo.action.next_page": "下一页", + "sakuraflow.action.restore": "恢复任务", + "sakuraflow.action.resume": "恢复", + "sakuraflow.action.pause": "暂停", + "sakuraflow.action.complete": "完工", + "sakuraflow.action.note": "笔记", + "sakuraflow.action.claim": "认领", + "sakuraflow.action.append": "追加", + "sakuraflow.action.modify": "修改", + "sakuraflow.action.remove": "移除", + "sakuraflow.action.prev_page": "上一页", + "sakuraflow.action.next_page": "下一页", - "todo.status.done": "已完成", - "todo.status.in_progress": "进行中", - "todo.status.on_hold": "挂起", + "sakuraflow.status.done": "已完成", + "sakuraflow.status.in_progress": "进行中", + "sakuraflow.status.on_hold": "挂起", - "todo.priority.very_high": "非常高", - "todo.priority.high": "高", - "todo.priority.medium": "中", - "todo.priority.low": "低", - "todo.priority.very_low": "非常低", + "sakuraflow.priority.very_high": "非常高", + "sakuraflow.priority.high": "高", + "sakuraflow.priority.medium": "中", + "sakuraflow.priority.low": "低", + "sakuraflow.priority.very_low": "非常低", - "todo.ui.hover.latest_progress": "最新进展", - "todo.ui.hover.waiting_record": "等待记录", - "todo.ui.hover.click_to_action": "点击{0}{1}", - "todo.ui.hover.remove_hint": "使用 {0} remove 移除项", + "sakuraflow.ui.hover.latest_progress": "最新进展", + "sakuraflow.ui.hover.waiting_record": "等待记录", + "sakuraflow.ui.hover.click_to_action": "点击{0}{1}", + "sakuraflow.ui.hover.remove_hint": "使用 {0} remove 移除项", - "todo.ui.info.header": "任务详情 #{0}", - "todo.ui.info.no_desc": "暂无描述", - "todo.ui.info.progress_header": "任务进度记录", - "todo.ui.info.no_records": "暂无记录", - "todo.ui.info.invalid_dep": "已失效", + "sakuraflow.ui.info.header": "任务详情 #{0}", + "sakuraflow.ui.info.no_desc": "暂无描述", + "sakuraflow.ui.info.progress_header": "任务进度记录", + "sakuraflow.ui.info.no_records": "暂无记录", + "sakuraflow.ui.info.invalid_dep": "已失效", - "todo.help.header": "TodoList 指令帮助", - "todo.help.hint": "提示:点击可填充指令至聊天栏;鼠标移至指令上方查看详情", - "todo.help.list": "查看进行中任务清单", - "todo.help.archive": "查看已完成归档记录", - "todo.help.search": "搜索任务 (支持多条件: t=标题 s=!Done)", - "todo.help.add": "立项一个新的任务", - "todo.help.info": "查询特定任务的详细信息", - "todo.help.note": "追加一条任务进度记录", - "todo.help.set": "修改任务的核心属性", - "todo.help.append": "向任务追加协作人或依赖", - "todo.help.remove": "从任务中移除协作人或依赖", - "todo.help.pause": "挂起当前任务", - "todo.help.resume": "恢复挂起任务", - "todo.help.complete": "标记任务完工并归档", - "todo.help.restore": "将已完成任务重新激活", + "sakuraflow.help.header": "TodoList 指令帮助", + "sakuraflow.help.hint": "提示:点击可填充指令至聊天栏;鼠标移至指令上方查看详情", + "sakuraflow.help.list": "查看进行中任务清单", + "sakuraflow.help.archive": "查看已完成归档记录", + "sakuraflow.help.search": "搜索任务 (支持多条件: t=标题 s=!Done)", + "sakuraflow.help.add": "立项一个新的任务", + "sakuraflow.help.info": "查询特定任务的详细信息", + "sakuraflow.help.note": "追加一条任务进度记录", + "sakuraflow.help.set": "修改任务的核心属性", + "sakuraflow.help.append": "向任务追加协作人或依赖", + "sakuraflow.help.remove": "从任务中移除协作人或依赖", + "sakuraflow.help.pause": "挂起当前任务", + "sakuraflow.help.resume": "恢复挂起任务", + "sakuraflow.help.complete": "标记任务完工并归档", + "sakuraflow.help.restore": "将已完成任务重新激活", - "todo.help.desc.set.main": "修改任务属性。电压(t): 0-14对应(ULV-MAX);优先级(p): 0=Very High, 4=Very Low", - "todo.help.available_props": "可用属性:", - "todo.help.desc.list.main": "向任务的列表字段中追加或移除项。依赖项需为有效的任务ID。", - "todo.help.available_lists": "可用列表:", + "sakuraflow.help.desc.set.main": "修改任务属性。电压(t): 0-14对应(ULV-MAX);优先级(p): 0=Very High, 4=Very Low", + "sakuraflow.help.available_props": "可用属性:", + "sakuraflow.help.desc.list.main": "向任务的列表字段中追加或移除项。依赖项需为有效的任务ID。", + "sakuraflow.help.available_lists": "可用列表:", - "todo.welcome.header": "TodoList", - "todo.welcome.line1": "欢迎使用待办事项管理插件。", - "todo.welcome.line2": "本插件能够协助管理复杂的流程,支持任务追踪、等级标记及协作管理。", - "todo.welcome.btn_line": "可用快捷操作", - "todo.welcome.btn.help": "查看帮助", - "todo.welcome.hover.help": "点击查看帮助", - "todo.welcome.btn.list": "任务列表", - "todo.welcome.hover.list": "点击查看未完成任务", - "todo.welcome.btn.add": "新建任务", - "todo.welcome.hover.add": "点击填充新建指令", + "sakuraflow.welcome.header": "TodoList", + "sakuraflow.welcome.line1": "欢迎使用待办事项管理插件。", + "sakuraflow.welcome.line2": "本插件能够协助管理复杂的流程,支持任务追踪、等级标记及协作管理。", + "sakuraflow.welcome.btn_line": "可用快捷操作", + "sakuraflow.welcome.btn.help": "查看帮助", + "sakuraflow.welcome.hover.help": "点击查看帮助", + "sakuraflow.welcome.btn.list": "任务列表", + "sakuraflow.welcome.hover.list": "点击查看未完成任务", + "sakuraflow.welcome.btn.add": "新建任务", + "sakuraflow.welcome.hover.add": "点击填充新建指令", - "todo.list.header": "任务列表", - "todo.list.empty": "当前没有进行中的任务", - "todo.archive.header": "已归档任务", - "todo.archive.empty": "归档记录为空", - "todo.search.header": "搜索结果", - "todo.search.empty": "未找到匹配的任务", - "todo.search.more_results": "... 还有 {0} 条结果", - "todo.search.cache_expired": "搜索缓存已过期,请重新输入查询条件", + "sakuraflow.list.header": "任务列表", + "sakuraflow.list.empty": "当前没有进行中的任务", + "sakuraflow.archive.header": "已归档任务", + "sakuraflow.archive.empty": "归档记录为空", + "sakuraflow.search.header": "搜索结果", + "sakuraflow.search.empty": "未找到匹配的任务", + "sakuraflow.search.more_results": "... 还有 {0} 条结果", + "sakuraflow.search.cache_expired": "搜索缓存已过期,请重新输入查询条件", - "todo.msg.add_success": "任务 {0} 已成功立项!", - "todo.msg.not_found": "未找到任务 ID", - "todo.msg.invalid_list_alias": "无效列表别称: {0}", - "todo.msg.dep_not_found": "引用错误:任务 #{0} 不存在,无法添加为前置依赖", - "todo.msg.append_success": "已向任务 #{0} 列表 {1} 追加: {2}", - "todo.msg.remove_success": "已从任务 #{0} 的 {1} 中移除: {2}", - "todo.msg.remove_failed": "移除失败:项 {0} 不在列表内或任务 ID 错误", - "todo.msg.invalid_prop_alias": "无效属性别称: {0}", - "todo.msg.invalid_tier": "无效的电压等级!请输入 0-{0} 或名称: {1}", - "todo.msg.invalid_priority": "无效优先级!请输入 0-4 或名称: {0}", - "todo.msg.invalid_status": "无效状态!请输入: {0}", - "todo.msg.set_success": "任务 #{0} 属性 {1} 已更新为 {2}", - "todo.msg.note_success": "进度已记录至任务 #{0}", - "todo.msg.restore_success": "任务 #{0} 已从归档中恢复至进行中状态 ▶", - "todo.msg.complete_success": "任务 #{0} 已完工并移入归档 ✔", - "todo.msg.pause_success": "任务 #{0} 已标记为暂停 ⏸", - "todo.msg.resume_success": "任务 #{0} 已恢复运行 ▶", - "todo.msg.default_tier_success": "默认电压等级已设置为: {0}", - "todo.msg.unknown_error": "未知错误: {0}" + "sakuraflow.msg.add_success": "任务 {0} 已成功立项!", + "sakuraflow.msg.not_found": "未找到任务 ID", + "sakuraflow.msg.invalid_list_alias": "无效列表别称: {0}", + "sakuraflow.msg.dep_not_found": "引用错误:任务 #{0} 不存在,无法添加为前置依赖", + "sakuraflow.msg.append_success": "已向任务 #{0} 列表 {1} 追加: {2}", + "sakuraflow.msg.remove_success": "已从任务 #{0} 的 {1} 中移除: {2}", + "sakuraflow.msg.remove_failed": "移除失败:项 {0} 不在列表内或任务 ID 错误", + "sakuraflow.msg.invalid_prop_alias": "无效属性别称: {0}", + "sakuraflow.msg.invalid_tier": "无效的电压等级!请输入 0-{0} 或名称: {1}", + "sakuraflow.msg.invalid_priority": "无效优先级!请输入 0-4 或名称: {0}", + "sakuraflow.msg.invalid_status": "无效状态!请输入: {0}", + "sakuraflow.msg.set_success": "任务 #{0} 属性 {1} 已更新为 {2}", + "sakuraflow.msg.note_success": "进度已记录至任务 #{0}", + "sakuraflow.msg.restore_success": "任务 #{0} 已从归档中恢复至进行中状态 ▶", + "sakuraflow.msg.complete_success": "任务 #{0} 已完工并移入归档 ✔", + "sakuraflow.msg.pause_success": "任务 #{0} 已标记为暂停 ⏸", + "sakuraflow.msg.resume_success": "任务 #{0} 已恢复运行 ▶", + "sakuraflow.msg.default_tier_success": "默认电压等级已设置为: {0}", + "sakuraflow.msg.unknown_error": "未知错误: {0}" } \ No newline at end of file diff --git a/sakura_flow/__init__.py b/sakura_flow/__init__.py index cf8c0ce..9d7b73b 100644 --- a/sakura_flow/__init__.py +++ b/sakura_flow/__init__.py @@ -1,4 +1,4 @@ -from mcdreforged.api.all import * +from mcdreforged.api.all import PluginServerInterface import os from .manager import TodoManager diff --git a/sakura_flow/constants.py b/sakura_flow/constants.py index 9768087..63f632a 100644 --- a/sakura_flow/constants.py +++ b/sakura_flow/constants.py @@ -1,5 +1,6 @@ from mcdreforged.api.all import RColor, RText -from .enums import Status, Priority + +from .enums import Priority COMMAND_PREFIX = "!!todo" PAGE_SIZE = 8 diff --git a/sakura_flow/controller.py b/sakura_flow/controller.py index 477daa5..a61bf42 100644 --- a/sakura_flow/controller.py +++ b/sakura_flow/controller.py @@ -1,8 +1,10 @@ -from typing import Optional, List, Dict, Any import time -from .manager import TodoManager +from typing import Optional, Dict, Any + +from .constants import PROP_ALIASES, LIST_PROP_ALIASES from .enums import Status, Tier, Priority -from .constants import PROP_ALIASES, LIST_PROP_ALIASES, GT_TIERS +from .manager import TodoManager + class SearchCache: def __init__(self, ttl: int = 300): @@ -140,26 +142,26 @@ def set_property(self, task_id: str, prop_alias: str, value: str, editor: str) - """ real_prop = PROP_ALIASES.get(prop_alias.lower()) if not real_prop: - return False, None, 'todo.msg.invalid_prop_alias' + return False, None, 'sakuraflow.msg.invalid_prop_alias' processed_val = value if real_prop == "tier": validated = Tier.validate(value) if not validated: - return False, None, 'todo.msg.invalid_tier' + return False, None, 'sakuraflow.msg.invalid_tier' processed_val = validated elif real_prop == "priority": validated = Priority.validate(value) if not validated: - return False, None, 'todo.msg.invalid_priority' + return False, None, 'sakuraflow.msg.invalid_priority' processed_val = validated elif real_prop == "status": validated = Status.validate(value) if not validated: - return False, None, 'todo.msg.invalid_status' + return False, None, 'sakuraflow.msg.invalid_status' processed_val = validated success = self.manager.update_task(task_id, real_prop, processed_val, editor) @@ -172,10 +174,10 @@ def append_list_property(self, task_id: str, list_alias: str, value: str, editor """ real_prop = LIST_PROP_ALIASES.get(list_alias.lower()) if not real_prop: - return False, 'todo.msg.invalid_list_alias' + return False, 'sakuraflow.msg.invalid_list_alias' if real_prop == "dependencies" and value not in self.manager.data["tasks"]: - return False, 'todo.msg.dep_not_found' + return False, 'sakuraflow.msg.dep_not_found' success = self.manager.update_task(task_id, real_prop, value, editor) return success, None @@ -187,7 +189,7 @@ def remove_list_property(self, task_id: str, list_alias: str, value: str, editor """ real_prop = LIST_PROP_ALIASES.get(list_alias.lower()) if not real_prop: - return False, 'todo.msg.invalid_list_alias' + return False, 'sakuraflow.msg.invalid_list_alias' success = self.manager.remove_item(task_id, real_prop, value, editor) return success, None diff --git a/sakura_flow/enums.py b/sakura_flow/enums.py index 3ef42a4..e022d1b 100644 --- a/sakura_flow/enums.py +++ b/sakura_flow/enums.py @@ -83,19 +83,19 @@ def validate(cls, val: str) -> Optional[str]: class Status(BaseProperty): - DONE = ("Done", RColor.green, ['done', 'd', 'finished', 'finish', 'complete', 'completed'], "todo.status.done") + DONE = ("Done", RColor.green, ['done', 'd', 'finished', 'finish', 'complete', 'completed'], "sakuraflow.status.done") IN_PROGRESS = ("In Progress", RColor.aqua, ['in progress', 'inprogress', 'ip', 'progress', 'p', 'doing'], - "todo.status.in_progress") + "sakuraflow.status.in_progress") ON_HOLD = ("On Hold", RColor.red, ['on hold', 'onhold', 'hold', 'oh', 'h', 'pause', 'paused', 'waiting'], - "todo.status.on_hold") + "sakuraflow.status.on_hold") class Priority(BaseProperty): - VERY_HIGH = ("Very High", RColor.dark_red, ['veryhigh', 'vh', '0'], "todo.priority.very_high") - HIGH = ("High", RColor.red, ['high', 'h', '1'], "todo.priority.high") - MEDIUM = ("Medium", RColor.yellow, ['medium', 'med', 'm', '2'], "todo.priority.medium") - LOW = ("Low", RColor.green, ['low', 'l', '3'], "todo.priority.low") - VERY_LOW = ("Very Low", RColor.gray, ['verylow', 'vl', '4'], "todo.priority.very_low") + VERY_HIGH = ("Very High", RColor.dark_red, ['veryhigh', 'vh', '0'], "sakuraflow.priority.very_high") + HIGH = ("High", RColor.red, ['high', 'h', '1'], "sakuraflow.priority.high") + MEDIUM = ("Medium", RColor.yellow, ['medium', 'med', 'm', '2'], "sakuraflow.priority.medium") + LOW = ("Low", RColor.green, ['low', 'l', '3'], "sakuraflow.priority.low") + VERY_LOW = ("Very Low", RColor.gray, ['verylow', 'vl', '4'], "sakuraflow.priority.very_low") class Tier(BaseProperty): diff --git a/sakura_flow/interface.py b/sakura_flow/interface.py index 611e583..045de0a 100644 --- a/sakura_flow/interface.py +++ b/sakura_flow/interface.py @@ -1,8 +1,7 @@ -from mcdreforged.api.all import * +from mcdreforged.api.all import RTextBase, RText, RColor, RTextList, ServerInterface, CommandSource, RAction, RStyle from . import TodoManager -from .constants import * -from .constants import PAGE_SIZE +from .constants import COMMAND_PREFIX, PAGE_SIZE, TASK_PROPERTIES, LIST_PROPERTIES, COLON from .enums import Status, Tier, Priority from .utils import Utils, ItemizeBuilder @@ -43,24 +42,24 @@ def create_hover_info(tid: str, task: dict, tasks_db: dict, server: ServerInterf prio = task.get('priority', 'Medium') collabs = task.get('collaborators', []) - collab_text = Utils.list_to_rtext(collabs) if collabs else server.tr('todo.common.unassigned') + collab_text = Utils.list_to_rtext(collabs) if collabs else server.tr('sakuraflow.common.unassigned') return RTextList( - RText(f"{server.tr('todo.common.task')}: {task['title']}\n", color=RColor.yellow), - RText(f"{server.tr('todo.common.creator')}: {task.get('creator', server.tr('todo.common.unknown'))}\n", + RText(f"{server.tr('sakuraflow.common.task')}: {task['title']}\n", color=RColor.yellow), + RText(f"{server.tr('sakuraflow.common.creator')}: {task.get('creator', server.tr('sakuraflow.common.unknown'))}\n", color=RColor.gray), - RText(f"{server.tr('todo.common.tier')}: ", color=RColor.gray), + RText(f"{server.tr('sakuraflow.common.tier')}: ", color=RColor.gray), Tier.get_rtext(tier), "\n", - RText(f"{server.tr('todo.common.priority')}: ", color=RColor.gray), + RText(f"{server.tr('sakuraflow.common.priority')}: ", color=RColor.gray), Priority.get_rtext(prio, server), "\n", - RText(f"{server.tr('todo.common.collaborators')}: ", color=RColor.gray), collab_text, "\n", - RText(f"{server.tr('todo.common.dependencies')}: ", color=RColor.gray), - Utils.list_to_rtext(dep_display) if dep_display else RText(server.tr('todo.common.none'), + RText(f"{server.tr('sakuraflow.common.collaborators')}: ", color=RColor.gray), collab_text, "\n", + RText(f"{server.tr('sakuraflow.common.dependencies')}: ", color=RColor.gray), + Utils.list_to_rtext(dep_display) if dep_display else RText(server.tr('sakuraflow.common.none'), color=RColor.gray), "\n", RText("-" * 25 + "\n"), RText( - f"{server.tr('todo.ui.hover.latest_progress')}: {task['notes'][-1]['content'] if task.get('notes') else server.tr('todo.ui.hover.waiting_record')}", + f"{server.tr('sakuraflow.ui.hover.latest_progress')}: {task['notes'][-1]['content'] if task.get('notes') else server.tr('sakuraflow.ui.hover.waiting_record')}", color=RColor.gray) ) @@ -79,19 +78,19 @@ def render_task_line(tid: str, task: dict, tasks_db: dict, server: ServerInterfa btns = RTextList() if is_done: btns.append( - Utils.create_button("↺", RColor.blue, server.tr('todo.action.restore'), f"{COMMAND_PREFIX} restore {tid}")) + Utils.create_button("↺", RColor.blue, server.tr('sakuraflow.action.restore'), f"{COMMAND_PREFIX} restore {tid}")) else: is_paused = status_str == Status.ON_HOLD.value - toggle_btn = Utils.create_button("▶", RColor.green, server.tr('todo.action.resume'), + toggle_btn = Utils.create_button("▶", RColor.green, server.tr('sakuraflow.action.resume'), f"{COMMAND_PREFIX} resume {tid}") if is_paused else \ - Utils.create_button("⏸", RColor.yellow, server.tr('todo.action.pause'), f"{COMMAND_PREFIX} pause {tid}") - complete_btn = Utils.create_button("✔", RColor.green, server.tr('todo.action.complete'), + Utils.create_button("⏸", RColor.yellow, server.tr('sakuraflow.action.pause'), f"{COMMAND_PREFIX} pause {tid}") + complete_btn = Utils.create_button("✔", RColor.green, server.tr('sakuraflow.action.complete'), f"{COMMAND_PREFIX} complete {tid}") - note_btn = Utils.create_button("✎", RColor.aqua, server.tr('todo.action.note'), f"{COMMAND_PREFIX} note {tid} ") + note_btn = Utils.create_button("✎", RColor.aqua, server.tr('sakuraflow.action.note'), f"{COMMAND_PREFIX} note {tid} ") btns.append(toggle_btn, " ", complete_btn, " ", note_btn) if source.is_player: - claim_btn = Utils.create_button("★", RColor.gold, server.tr('todo.action.claim'), f"{COMMAND_PREFIX} set {tid} collaborators {source.player}") + claim_btn = Utils.create_button("★", RColor.gold, server.tr('sakuraflow.action.claim'), f"{COMMAND_PREFIX} set {tid} collaborators {source.player}") btns.append(claim_btn) return RTextList( @@ -110,12 +109,12 @@ def _render_info_row(tid: str, label: str, value: RTextBase | str, cmd: str, ser """ LABEL_COLOR = RColor.gray action_type = "append" if is_list else "set" - hint = server.tr('todo.action.append') if is_list else server.tr('todo.action.modify') + hint = server.tr('sakuraflow.action.append') if is_list else server.tr('sakuraflow.action.modify') cmd_str = f"{COMMAND_PREFIX} {action_type} {tid} {cmd} " - hover = server.tr('todo.ui.hover.click_to_action', hint, label) + hover = server.tr('sakuraflow.ui.hover.click_to_action', hint, label) if is_list: - hover += "\n" + server.tr('todo.ui.hover.remove_hint', COMMAND_PREFIX) + hover += "\n" + server.tr('sakuraflow.ui.hover.remove_hint', COMMAND_PREFIX) # 标签部分:永远保持点击触发修改指令 label_component = RText(f"{label}: ", color=LABEL_COLOR).h(hover).c(RAction.suggest_command, cmd_str) @@ -137,7 +136,7 @@ def render_task_info(tid: str, task: dict, tasks_db: dict, server: ServerInterfa # 依赖列表特殊渲染逻辑 deps = task.get("dependencies", []) if not deps: - dep_list = RText(server.tr('todo.common.none'), color=RColor.gray) + dep_list = RText(server.tr('sakuraflow.common.none'), color=RColor.gray) else: dep_items = [] for d_id in deps: @@ -155,7 +154,7 @@ def render_task_info(tid: str, task: dict, tasks_db: dict, server: ServerInterfa .c(RAction.suggest_command, f"{COMMAND_PREFIX} info {d_id}") ) else: - dep_items.append(RText(f"#{d_id}{server.tr('todo.ui.info.invalid_dep')}", color=RColor.red)) + dep_items.append(RText(f"#{d_id}{server.tr('sakuraflow.ui.info.invalid_dep')}", color=RColor.red)) dep_list = Utils.list_to_rtext(dep_items) # 日志内容构建 @@ -169,35 +168,35 @@ def render_task_info(tid: str, task: dict, tasks_db: dict, server: ServerInterfa RText(f"{n['content']}\n") )) else: - notes_content.append(RText(f" {server.tr('todo.ui.info.no_records')}\n", color=RColor.dark_gray)) + notes_content.append(RText(f" {server.tr('sakuraflow.ui.info.no_records')}\n", color=RColor.dark_gray)) collabs = task.get('collaborators', []) - collab_val = Utils.list_to_rtext(collabs) if collabs else server.tr('todo.common.unassigned') + collab_val = Utils.list_to_rtext(collabs) if collabs else server.tr('sakuraflow.common.unassigned') description = task.get('description', '') if not description: - description = server.tr('todo.ui.info.no_desc') + description = server.tr('sakuraflow.ui.info.no_desc') return RTextList( - UI.make_dividing_line(server.tr('todo.ui.info.header', tid)), - UI._render_info_row(tid, server.tr('todo.common.title'), task['title'], "title", server), - RText(f"{server.tr('todo.common.creator')}: ", color=RColor.gray), - RText(f"{task.get('creator', server.tr('todo.common.unknown'))}\n", color=RColor.white), - UI._render_info_row(tid, server.tr('todo.common.status'), Status.get_rtext(task['status'], server), + UI.make_dividing_line(server.tr('sakuraflow.ui.info.header', tid)), + UI._render_info_row(tid, server.tr('sakuraflow.common.title'), task['title'], "title", server), + RText(f"{server.tr('sakuraflow.common.creator')}: ", color=RColor.gray), + RText(f"{task.get('creator', server.tr('sakuraflow.common.unknown'))}\n", color=RColor.white), + UI._render_info_row(tid, server.tr('sakuraflow.common.status'), Status.get_rtext(task['status'], server), "status", server, value_color=Status.get_color(task['status'])), - UI._render_info_row(tid, server.tr('todo.common.tier'), Tier.get_rtext(task['tier']), + UI._render_info_row(tid, server.tr('sakuraflow.common.tier'), Tier.get_rtext(task['tier']), "tier", server, value_color=Tier.get_color(task['tier'])), - UI._render_info_row(tid, server.tr('todo.common.priority'), Priority.get_rtext(task['priority'], server), + UI._render_info_row(tid, server.tr('sakuraflow.common.priority'), Priority.get_rtext(task['priority'], server), "priority", server, value_color=Priority.get_color(task['priority'])), - UI._render_info_row(tid, server.tr('todo.common.collaborators'), collab_val, "collaborators", server, + UI._render_info_row(tid, server.tr('sakuraflow.common.collaborators'), collab_val, "collaborators", server, is_list=True), - UI._render_info_row(tid, server.tr('todo.common.dependencies'), dep_list, "dependency", server, + UI._render_info_row(tid, server.tr('sakuraflow.common.dependencies'), dep_list, "dependency", server, is_list=True), - UI._render_info_row(tid, server.tr('todo.common.description'), + UI._render_info_row(tid, server.tr('sakuraflow.common.description'), description, "description", server), RText("-" * 35 + "\n", color=RColor.dark_gray), - RText(f"{server.tr('todo.ui.info.progress_header')}\n", color=RColor.gold), + RText(f"{server.tr('sakuraflow.ui.info.progress_header')}\n", color=RColor.gold), *notes_content, UI.make_dividing_line(newline=False) ) @@ -211,12 +210,12 @@ def help_line(cmd: str, desc: str, usage: str = "", full_desc: RTextBase | str = hover_content = RTextList() # 用法展示 - hover_content.append(RText(f"{server.tr('todo.common.usage')}: ", color=RColor.gray)) + hover_content.append(RText(f"{server.tr('sakuraflow.common.usage')}: ", color=RColor.gray)) hover_content.append(RText(f"{COMMAND_PREFIX} {cmd} {usage}\n", color=RColor.aqua)) # 缩写展示 if abbr: - hover_content.append(RText(f"{server.tr('todo.common.alias')}: ", color=RColor.gray)) + hover_content.append(RText(f"{server.tr('sakuraflow.common.alias')}: ", color=RColor.gray)) hover_content.append(RText(f"{COMMAND_PREFIX} {abbr}\n", color=RColor.aqua)) hover_content.append(RText("-" * 20 + "\n", color=RColor.dark_gray)) @@ -243,7 +242,7 @@ def build_props_info(header_key: str, props_dict: dict) -> RTextList: builder = ItemizeBuilder() items = list(props_dict.items()) for i, (prop, aliases) in enumerate(items): - prop_name = server.tr(f"todo.prop.{prop}") + prop_name = server.tr(f"sakuraflow.prop.{prop}") alias_items = [RText(a, color=RColor.aqua) for a in sorted(aliases)] alias_list = Utils.list_to_rtext(alias_items) @@ -259,37 +258,37 @@ def build_props_info(header_key: str, props_dict: dict) -> RTextList: return result properties_info = RTextList( - server.tr('todo.help.desc.set.main'), "\n\n", - build_props_info('todo.help.available_props', TASK_PROPERTIES) + server.tr('sakuraflow.help.desc.set.main'), "\n\n", + build_props_info('sakuraflow.help.available_props', TASK_PROPERTIES) ) list_properties_info = RTextList( - server.tr('todo.help.desc.list.main'), "\n\n", - build_props_info('todo.help.available_lists', LIST_PROPERTIES) + server.tr('sakuraflow.help.desc.list.main'), "\n\n", + build_props_info('sakuraflow.help.available_lists', LIST_PROPERTIES) ) return RTextList( - UI.make_dividing_line(server.tr('todo.help.header')), - RText(f"{server.tr('todo.help.hint')}\n", color=RColor.gray, styles=RStyle.italic), - - help_line("list", server.tr('todo.help.list'), usage="", abbr="l"), - help_line("archive", server.tr('todo.help.archive'), usage="", abbr="ar"), - help_line("search", server.tr('todo.help.search'), usage="", abbr="find"), - help_line("add", server.tr('todo.help.add'), usage="", abbr="a"), - help_line("info", server.tr('todo.help.info'), usage="<id>", abbr="i"), - help_line("note", server.tr('todo.help.note'), usage="<id> <content>", abbr="n"), - help_line("set", server.tr('todo.help.set'), usage="<id> <prop> <value>", full_desc=properties_info, + UI.make_dividing_line(server.tr('sakuraflow.help.header')), + RText(f"{server.tr('sakuraflow.help.hint')}\n", color=RColor.gray, styles=RStyle.italic), + + help_line("list", server.tr('sakuraflow.help.list'), usage="", abbr="l"), + help_line("archive", server.tr('sakuraflow.help.archive'), usage="", abbr="ar"), + help_line("search", server.tr('sakuraflow.help.search'), usage="<query>", abbr="find"), + help_line("add", server.tr('sakuraflow.help.add'), usage="<title>", abbr="a"), + help_line("info", server.tr('sakuraflow.help.info'), usage="<id>", abbr="i"), + help_line("note", server.tr('sakuraflow.help.note'), usage="<id> <content>", abbr="n"), + help_line("set", server.tr('sakuraflow.help.set'), usage="<id> <prop> <value>", full_desc=properties_info, abbr="s"), - help_line("append", server.tr('todo.help.append'), usage="<id> <list> <value>", + help_line("append", server.tr('sakuraflow.help.append'), usage="<id> <list> <value>", full_desc=list_properties_info, abbr="ap"), - help_line("remove", server.tr('todo.help.remove'), usage="<id> <list> <value>", + help_line("remove", server.tr('sakuraflow.help.remove'), usage="<id> <list> <value>", full_desc=list_properties_info, abbr="rm"), - help_line("pause", server.tr('todo.help.pause'), usage="<id>"), - help_line("resume", server.tr('todo.help.resume'), usage="<id>"), - help_line("complete", server.tr('todo.help.complete'), usage="<id>"), - help_line("restore", server.tr('todo.help.restore'), usage="<id>"), + help_line("pause", server.tr('sakuraflow.help.pause'), usage="<id>"), + help_line("resume", server.tr('sakuraflow.help.resume'), usage="<id>"), + help_line("complete", server.tr('sakuraflow.help.complete'), usage="<id>"), + help_line("restore", server.tr('sakuraflow.help.restore'), usage="<id>"), UI.make_dividing_line(newline=False) ) @@ -298,18 +297,18 @@ def build_props_info(header_key: str, props_dict: dict) -> RTextList: def render_welcome(server: ServerInterface) -> RTextBase: """渲染欢迎界面""" return RTextList( - UI.make_dividing_line(server.tr('todo.welcome.header')), - RText(f"{server.tr('todo.welcome.line1')}\n", color=RColor.white), - RText(f"{server.tr('todo.welcome.line2')}\n", color=RColor.gray), - RText(f"{server.tr('todo.welcome.btn_line')}", color=RColor.gray), + UI.make_dividing_line(server.tr('sakuraflow.welcome.header')), + RText(f"{server.tr('sakuraflow.welcome.line1')}\n", color=RColor.white), + RText(f"{server.tr('sakuraflow.welcome.line2')}\n", color=RColor.gray), + RText(f"{server.tr('sakuraflow.welcome.btn_line')}", color=RColor.gray), COLON, - Utils.create_button(server.tr('todo.welcome.btn.help'), RColor.aqua, server.tr('todo.welcome.hover.help'), + Utils.create_button(server.tr('sakuraflow.welcome.btn.help'), RColor.aqua, server.tr('sakuraflow.welcome.hover.help'), f"{COMMAND_PREFIX} help"), " ", - Utils.create_button(server.tr('todo.welcome.btn.list'), RColor.green, server.tr('todo.welcome.hover.list'), + Utils.create_button(server.tr('sakuraflow.welcome.btn.list'), RColor.green, server.tr('sakuraflow.welcome.hover.list'), f"{COMMAND_PREFIX} list"), " ", - Utils.create_button(server.tr('todo.welcome.btn.add'), RColor.yellow, server.tr('todo.welcome.hover.add'), + Utils.create_button(server.tr('sakuraflow.welcome.btn.add'), RColor.yellow, server.tr('sakuraflow.welcome.hover.add'), f"{COMMAND_PREFIX} add ") ) @@ -359,7 +358,7 @@ def render_paged_list(source: CommandSource, tasks: dict, manager: TodoManager, # 上一页按钮 if page > 0: prev_cmd = f"{COMMAND_PREFIX} {cmd_prefix} {page}" - footer.append(Utils.create_button("<<", RColor.aqua, server.tr("todo.action.prev_page"), prev_cmd)) + footer.append(Utils.create_button("<<", RColor.aqua, server.tr("sakuraflow.action.prev_page"), prev_cmd)) else: footer.append(RText("[<<]", color=RColor.gray)) @@ -368,7 +367,7 @@ def render_paged_list(source: CommandSource, tasks: dict, manager: TodoManager, # 下一页按钮 if page < total_pages - 1: next_cmd = f"{COMMAND_PREFIX} {cmd_prefix} {page + 2}" - footer.append(Utils.create_button(">>", RColor.aqua, server.tr("todo.action.next_page"), next_cmd)) + footer.append(Utils.create_button(">>", RColor.aqua, server.tr("sakuraflow.action.next_page"), next_cmd)) else: footer.append(RText("[>>]", color=RColor.gray)) diff --git a/sakura_flow/mcdr_entry.py b/sakura_flow/mcdr_entry.py index 5437ad7..a3f803d 100644 --- a/sakura_flow/mcdr_entry.py +++ b/sakura_flow/mcdr_entry.py @@ -1,5 +1,5 @@ -from mcdreforged.api.all import * -from mcdreforged.api.command import * +from mcdreforged.api.all import PluginServerInterface, CommandSource, CommandContext, RText, RColor, RStyle +from mcdreforged.api.command import Literal, Integer, GreedyText, Text from .controller import TodoController from .interface import UI @@ -20,14 +20,14 @@ def on_list(source: CommandSource, context: CommandContext): page = context.get("page", 1) # 使用 search_tasks 获取非 Done 任务 tasks = controller.search_tasks({'status': '!Done'}) - UI.render_paged_list(source, tasks, controller.manager, 'todo.list.header', 'todo.list.empty', + UI.render_paged_list(source, tasks, controller.manager, 'sakuraflow.list.header', 'sakuraflow.list.empty', input_page=page, cmd_prefix="list") def on_archive(source: CommandSource, context: CommandContext): page = context.get("page", 1) # 使用 search_tasks 获取 Done 任务 tasks = controller.search_tasks({'status': 'Done'}) - UI.render_paged_list(source, tasks, controller.manager, 'todo.archive.header', 'todo.archive.empty', + UI.render_paged_list(source, tasks, controller.manager, 'sakuraflow.archive.header', 'sakuraflow.archive.empty', input_page=page, cmd_prefix="archive") def on_search(source: CommandSource, context: CommandContext): @@ -48,7 +48,7 @@ def on_search(source: CommandSource, context: CommandContext): results = controller.get_cached_search(player_key) if results is None: # 缓存过期或不存在,提示用户重新搜索 - source.reply(Utils.error_msg(server, 'todo.search.cache_expired')) + source.reply(Utils.error_msg(server, 'sakuraflow.search.cache_expired')) return else: # 执行新搜索 @@ -82,7 +82,7 @@ def on_search(source: CommandSource, context: CommandContext): page = 1 # 新搜索重置为第一页 # 渲染搜索结果 - UI.render_paged_list(source, results, controller.manager, 'todo.search.header', 'todo.search.empty', + UI.render_paged_list(source, results, controller.manager, 'sakuraflow.search.header', 'sakuraflow.search.empty', input_page=page, cmd_prefix="search") @@ -90,13 +90,13 @@ def on_add(source: CommandSource, context: CommandContext): creator = source.player if source.is_player else "Console" tid = controller.add_task(context['title'], creator) tid_text = RText(f"#{tid}", color=RColor.green, styles=RStyle.bold) - source.reply(Utils.info_msg(server, 'todo.msg.add_success', tid_text)) + source.reply(Utils.info_msg(server, 'sakuraflow.msg.add_success', tid_text)) def on_info(source: CommandSource, context: CommandContext): tid = str(context['id']) task = controller.get_task(tid) if not task: - source.reply(Utils.error_msg(server, 'todo.msg.not_found')) + source.reply(Utils.error_msg(server, 'sakuraflow.msg.not_found')) return source.reply(UI.render_task_info(tid, task, controller.manager.data["tasks"], server)) @@ -105,17 +105,17 @@ def on_set(source: CommandSource, context: CommandContext): success, val, err = controller.set_property(str(context['id']), context['prop'], context['value'], editor) if not success: - if err == 'todo.msg.invalid_tier': + if err == 'sakuraflow.msg.invalid_tier': tier_list = Utils.list_to_rtext([Tier.get_rtext(t.value) for t in Tier]) source.reply(Utils.error_msg(server, err, len(GT_TIERS) - 1, tier_list)) - elif err == 'todo.msg.invalid_priority': + elif err == 'sakuraflow.msg.invalid_priority': prio_list = Utils.list_to_rtext([Priority.get_rtext(p.value) for p in Priority]) source.reply(Utils.error_msg(server, err, prio_list)) - elif err == 'todo.msg.invalid_status': + elif err == 'sakuraflow.msg.invalid_status': status_list = Utils.list_to_rtext([Status.get_rtext(s.value) for s in Status]) source.reply(Utils.error_msg(server, err, status_list)) else: - source.reply(Utils.error_msg(server, err or 'todo.msg.unknown_error', context.get('prop'))) + source.reply(Utils.error_msg(server, err or 'sakuraflow.msg.unknown_error', context.get('prop'))) return # 成功后的 UI 反馈 @@ -128,35 +128,35 @@ def on_set(source: CommandSource, context: CommandContext): elif Priority.validate(val): rval = Priority.get_rtext(val, server) elif Status.validate(val): rval = Status.get_rtext(val, server) - source.reply(Utils.info_msg(server, 'todo.msg.set_success', context['id'], context['prop'], rval)) + source.reply(Utils.info_msg(server, 'sakuraflow.msg.set_success', context['id'], context['prop'], rval)) def on_append(source: CommandSource, context: CommandContext): editor = source.player if source.is_player else "Console" success, err = controller.append_list_property(str(context['id']), context['list_prop'], str(context['value']), editor) if not success: - if err == 'todo.msg.dep_not_found': + if err == 'sakuraflow.msg.dep_not_found': source.reply(Utils.error_msg(server, err, context['value'])) else: - source.reply(Utils.error_msg(server, err or 'todo.msg.unknown_error', context['list_prop'])) + source.reply(Utils.error_msg(server, err or 'sakuraflow.msg.unknown_error', context['list_prop'])) return - source.reply(Utils.info_msg(server, 'todo.msg.append_success', context['id'], context['list_prop'], context['value'])) + source.reply(Utils.info_msg(server, 'sakuraflow.msg.append_success', context['id'], context['list_prop'], context['value'])) def on_remove(source: CommandSource, context: CommandContext): editor = source.player if source.is_player else "Console" success, err = controller.remove_list_property(str(context['id']), context['list_prop'], str(context['value']), editor) if not success: - source.reply(Utils.error_msg(server, 'todo.msg.remove_failed', context['value'])) + source.reply(Utils.error_msg(server, 'sakuraflow.msg.remove_failed', context['value'])) return - source.reply(Utils.info_msg(server, 'todo.msg.remove_success', context['id'], context['list_prop'], context['value'])) + source.reply(Utils.info_msg(server, 'sakuraflow.msg.remove_success', context['id'], context['list_prop'], context['value'])) def on_note(source: CommandSource, context: CommandContext): author = source.player if source.is_player else "Console" if controller.add_note(str(context['id']), context['content'], author): - source.reply(Utils.info_msg(server, 'todo.msg.note_success', context['id'])) + source.reply(Utils.info_msg(server, 'sakuraflow.msg.note_success', context['id'])) def on_status_change(source: CommandSource, context: CommandContext, status: Status, msg_key: str): editor = source.player if source.is_player else "Console" @@ -165,10 +165,10 @@ def on_status_change(source: CommandSource, context: CommandContext, status: Sta def on_default_tier(source: CommandSource, context: CommandContext): if controller.set_default_tier(context['tier']): - source.reply(Utils.info_msg(server, 'todo.msg.default_tier_success', context['tier'])) + source.reply(Utils.info_msg(server, 'sakuraflow.msg.default_tier_success', context['tier'])) else: tier_list = Utils.list_to_rtext([Tier.get_rtext(t.value) for t in Tier]) - source.reply(Utils.error_msg(server, 'todo.msg.invalid_tier', len(GT_TIERS) - 1, tier_list)) + source.reply(Utils.error_msg(server, 'sakuraflow.msg.invalid_tier', len(GT_TIERS) - 1, tier_list)) # --- Command Tree Definition --- @@ -211,10 +211,10 @@ def on_default_tier(source: CommandSource, context: CommandContext): node_note = Literal('note').then(Text('id').then(GreedyText('content').runs(on_note))) node_note_alias = Literal('n').then(Text('id').then(GreedyText('content').runs(on_note))) - node_complete = Literal('complete').then(Text('id').runs(lambda s, c: on_status_change(s, c, Status.DONE, 'todo.msg.complete_success'))) - node_pause = Literal('pause').then(Text('id').runs(lambda s, c: on_status_change(s, c, Status.ON_HOLD, 'todo.msg.pause_success'))) - node_resume = Literal('resume').then(Text('id').runs(lambda s, c: on_status_change(s, c, Status.IN_PROGRESS, 'todo.msg.resume_success'))) - node_restore = Literal('restore').then(Text('id').runs(lambda s, c: on_status_change(s, c, Status.IN_PROGRESS, 'todo.msg.restore_success'))) + node_complete = Literal('complete').then(Text('id').runs(lambda s, c: on_status_change(s, c, Status.DONE, 'sakuraflow.msg.complete_success'))) + node_pause = Literal('pause').then(Text('id').runs(lambda s, c: on_status_change(s, c, Status.ON_HOLD, 'sakuraflow.msg.pause_success'))) + node_resume = Literal('resume').then(Text('id').runs(lambda s, c: on_status_change(s, c, Status.IN_PROGRESS, 'sakuraflow.msg.resume_success'))) + node_restore = Literal('restore').then(Text('id').runs(lambda s, c: on_status_change(s, c, Status.IN_PROGRESS, 'sakuraflow.msg.restore_success'))) node_default_tier = Literal('default_tier').then(Text('tier').runs(on_default_tier)) diff --git a/sakura_flow/utils.py b/sakura_flow/utils.py index 3a85492..d754804 100644 --- a/sakura_flow/utils.py +++ b/sakura_flow/utils.py @@ -1,9 +1,8 @@ -from typing import Optional, Any, List +from typing import Any, List -from mcdreforged.api.all import * +from mcdreforged.api.all import ServerInterface, RTextList, RText, RColor, RTextBase, RAction -from .constants import * -from .constants import ITEMIZE_PREFIX +from .constants import ITEMIZE_PREFIX, LIST_ITEM_SEPERATOR class Utils: diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..6d923bd --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,18 @@ +import pytest +from unittest.mock import MagicMock +from mcdreforged.api.all import PluginServerInterface + +@pytest.fixture +def mock_server(): + server = MagicMock(spec=PluginServerInterface) + # 模拟 register_command,避免报错 + server.register_command = MagicMock() + return server + +@pytest.fixture +def mock_controller(): + # 模拟 TodoController + # 这里不需要引入真实的 Controller 类,只需要一个有相应方法的 Mock 对象 + # 这样可以解耦测试 + controller = MagicMock() + return controller diff --git a/tests/test_command_registration.py b/tests/test_command_registration.py new file mode 100644 index 0000000..88179a5 --- /dev/null +++ b/tests/test_command_registration.py @@ -0,0 +1,26 @@ +import pytest +from sakura_flow.mcdr_entry import register_mcdr_commands +from mcdreforged.api.command import Literal + +def test_command_registration(mock_server, mock_controller): + """ + 测试命令注册流程是否正常,确保命令树构建没有语法错误 + """ + try: + register_mcdr_commands(mock_server, mock_controller) + except Exception as e: + pytest.fail(f"命令注册失败: {e}") + + # 验证是否调用了 server.register_command + assert mock_server.register_command.called, "未调用 server.register_command" + + # 获取注册的命令节点 + # call_args[0][0] 是第一个位置参数,即 node_root + node_root = mock_server.register_command.call_args[0][0] + + # 验证根节点是否正确 + assert isinstance(node_root, Literal) + + # 由于 MCDR 的 CommandNode 内部结构可能随版本变化且不一定公开 children 属性, + # 这里我们只验证根节点对象存在且类型正确。 + # 如果命令树构建逻辑有误(如 .then() 链式调用错误),上面的 register_mcdr_commands 通常会直接抛出异常。 diff --git a/tests/test_smoke.py b/tests/test_smoke.py new file mode 100644 index 0000000..0ac6829 --- /dev/null +++ b/tests/test_smoke.py @@ -0,0 +1,25 @@ +import os +import pytest + +# 定义项目根目录 +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +def test_plugin_structure(): + """ + 验证插件的基本结构和导入是否正常 + """ + try: + import sakura_flow + except ImportError as e: + pytest.fail(f"无法导入 sakura_flow 插件: {e}") + + # 验证必要的入口函数是否存在 + assert hasattr(sakura_flow, 'on_load'), "插件缺少 on_load 入口函数" + assert callable(sakura_flow.on_load), "on_load 必须是可调用的函数" + +def test_metadata_exists(): + """ + 验证插件元数据文件是否存在 + """ + metadata_file = os.path.join(PROJECT_ROOT, 'mcdreforged.plugin.json') + assert os.path.exists(metadata_file), "缺少 mcdreforged.plugin.json 文件" diff --git a/tests/test_translation_keys.py b/tests/test_translation_keys.py new file mode 100644 index 0000000..824dc62 --- /dev/null +++ b/tests/test_translation_keys.py @@ -0,0 +1,73 @@ +import os +import json +import ast +import pytest + +# 定义项目根目录 +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LANG_FILE = os.path.join(PROJECT_ROOT, 'lang', 'zh_cn.json') +SOURCE_DIR = os.path.join(PROJECT_ROOT, 'sakura_flow') + +def get_defined_keys(): + """读取语言文件并返回所有定义的键""" + if not os.path.exists(LANG_FILE): + pytest.fail(f"语言文件不存在: {LANG_FILE}") + + with open(LANG_FILE, 'r', encoding='utf-8') as f: + data = json.load(f) + return set(data.keys()) + +def scan_used_keys(): + """ + 使用 AST 扫描源代码中使用的翻译键 + 只收集硬编码的字符串常量,忽略 f-string 和动态拼接 + """ + used_keys = set() + + for root, _, files in os.walk(SOURCE_DIR): + for file in files: + if file.endswith('.py'): + file_path = os.path.join(root, file) + with open(file_path, 'r', encoding='utf-8') as f: + try: + tree = ast.parse(f.read(), filename=file_path) + except SyntaxError: + continue # 忽略语法错误的文件(如果有) + + for node in ast.walk(tree): + # 在 Python 3.8+ 中,字符串常量是 ast.Constant + if isinstance(node, ast.Constant) and isinstance(node.value, str): + val = node.value + # 筛选条件: + # 1. 以 sakuraflow. 开头 + # 2. 不以 . 结尾(排除类似 "sakuraflow.prop." 的动态前缀) + if val.startswith('sakuraflow.') and not val.endswith('.'): + used_keys.add(val) + + return used_keys + +def test_translation_keys_exist(): + """测试代码中引用的所有翻译键是否都在语言文件中定义""" + defined_keys = get_defined_keys() + used_keys = scan_used_keys() + + missing_keys = used_keys - defined_keys + + if missing_keys: + pytest.fail(f"发现 {len(missing_keys)} 个未定义的翻译键:\n" + "\n".join(missing_keys)) + +def test_unused_keys(): + """(可选) 测试是否有未使用的翻译键""" + # 这个测试通常作为警告而不是错误,因为有些键可能只在动态生成时使用 + defined_keys = get_defined_keys() + used_keys = scan_used_keys() + + unused = defined_keys - used_keys + # 这里我们只打印警告,不让测试失败 + if unused: + print(f"\n警告: 发现 {len(unused)} 个可能未使用的翻译键 (可能是动态调用):") + # 只打印前10个 + for k in list(unused)[:10]: + print(f" - {k}") + if len(unused) > 10: + print(" ...")