mofox-plugin-dev-toolkit 0.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. mofox_plugin_dev_toolkit-0.3.3.dist-info/METADATA +730 -0
  2. mofox_plugin_dev_toolkit-0.3.3.dist-info/RECORD +46 -0
  3. mofox_plugin_dev_toolkit-0.3.3.dist-info/WHEEL +5 -0
  4. mofox_plugin_dev_toolkit-0.3.3.dist-info/entry_points.txt +2 -0
  5. mofox_plugin_dev_toolkit-0.3.3.dist-info/licenses/LICENSE +674 -0
  6. mofox_plugin_dev_toolkit-0.3.3.dist-info/top_level.txt +1 -0
  7. mpdt/__init__.py +15 -0
  8. mpdt/__main__.py +8 -0
  9. mpdt/cli.py +316 -0
  10. mpdt/commands/__init__.py +9 -0
  11. mpdt/commands/check.py +498 -0
  12. mpdt/commands/dev.py +318 -0
  13. mpdt/commands/generate.py +448 -0
  14. mpdt/commands/init.py +686 -0
  15. mpdt/dev/bridge_plugin/__init__.py +17 -0
  16. mpdt/dev/bridge_plugin/cleanup_handler.py +65 -0
  17. mpdt/dev/bridge_plugin/dev_config.py +24 -0
  18. mpdt/dev/bridge_plugin/file_watcher.py +169 -0
  19. mpdt/dev/bridge_plugin/plugin.py +219 -0
  20. mpdt/templates/__init__.py +165 -0
  21. mpdt/templates/action_template.py +102 -0
  22. mpdt/templates/adapter_template.py +129 -0
  23. mpdt/templates/chatter_template.py +103 -0
  24. mpdt/templates/event_template.py +116 -0
  25. mpdt/templates/plus_command_template.py +150 -0
  26. mpdt/templates/prompt_template.py +92 -0
  27. mpdt/templates/router_template.py +175 -0
  28. mpdt/templates/tool_template.py +98 -0
  29. mpdt/utils/__init__.py +10 -0
  30. mpdt/utils/code_parser.py +401 -0
  31. mpdt/utils/color_printer.py +99 -0
  32. mpdt/utils/config_loader.py +171 -0
  33. mpdt/utils/config_manager.py +297 -0
  34. mpdt/utils/file_ops.py +207 -0
  35. mpdt/utils/license_generator.py +980 -0
  36. mpdt/utils/plugin_parser.py +195 -0
  37. mpdt/utils/template_engine.py +112 -0
  38. mpdt/validators/__init__.py +26 -0
  39. mpdt/validators/auto_fix_validator.py +990 -0
  40. mpdt/validators/base.py +129 -0
  41. mpdt/validators/component_validator.py +842 -0
  42. mpdt/validators/config_validator.py +119 -0
  43. mpdt/validators/metadata_validator.py +107 -0
  44. mpdt/validators/structure_validator.py +72 -0
  45. mpdt/validators/style_validator.py +117 -0
  46. mpdt/validators/type_validator.py +206 -0
@@ -0,0 +1,119 @@
1
+ """
2
+ 配置文件验证器
3
+ """
4
+
5
+ from ..utils.code_parser import CodeParser
6
+ from .base import BaseValidator, ValidationResult
7
+
8
+
9
+ class ConfigValidator(BaseValidator):
10
+ """配置文件验证器
11
+
12
+ 仅检查插件类中定义的 config_schema 是否正确
13
+ 不验证 config.toml 文件(因为它会在运行时自动生成)
14
+ """
15
+
16
+ def validate(self) -> ValidationResult:
17
+ """执行配置验证
18
+
19
+ Returns:
20
+ ValidationResult: 验证结果
21
+ """
22
+ # 获取插件名称
23
+ plugin_name = self._get_plugin_name()
24
+ if not plugin_name:
25
+ self.result.add_error("无法确定插件名称")
26
+ return self.result
27
+
28
+ plugin_file = self.plugin_path / "plugin.py"
29
+
30
+ if not plugin_file.exists():
31
+ self.result.add_error("插件文件不存在: plugin.py")
32
+ return self.result
33
+
34
+ # 解析 plugin.py 查找 config_schema
35
+ config_schema = self._extract_config_schema(plugin_file, plugin_name)
36
+
37
+ if config_schema is None:
38
+ # 没有定义 config_schema,这是正常的
39
+ self.result.add_warning(
40
+ "插件未定义配置 schema",
41
+ file_path="plugin.py",
42
+ suggestion="最好定义config_schema启用插件配置系统",
43
+ )
44
+ return self.result
45
+
46
+ # 验证 config_schema 的结构
47
+ if not config_schema:
48
+ self.result.add_warning(
49
+ "config_schema 已定义但为空",
50
+ file_path="plugin.py",
51
+ suggestion="最好往里面加入一些配置项",
52
+ )
53
+ return self.result
54
+
55
+ # 检查是否定义了 config_file_name
56
+ has_config_file_name = self._check_config_file_name(plugin_file, plugin_name)
57
+ if not has_config_file_name:
58
+ self.result.add_warning(
59
+ "定义了 config_schema 但未定义 config_file_name",
60
+ file_path="plugin.py",
61
+ suggestion="请在插件类中添加: config_file_name = 'config.toml'",
62
+ )
63
+
64
+ # 验证每个配置节
65
+ for section_name, section_content in config_schema.items():
66
+ if not section_name:
67
+ self.result.add_error(
68
+ "config_schema 中存在空的配置节名",
69
+ file_path="plugin.py",
70
+ )
71
+ elif not isinstance(section_content, dict):
72
+ self.result.add_warning(
73
+ f"config_schema 中的 [{section_name}] 节格式不正确",
74
+ file_path="plugin.py",
75
+ suggestion="每个配置节应该是一个字典,包含 ConfigField 定义",
76
+ )
77
+
78
+ self.result.add_info(f"config_schema 定义了 {len(config_schema)} 个配置节")
79
+ return self.result
80
+
81
+ def _check_config_file_name(self, plugin_file, plugin_name: str) -> bool:
82
+ """检查是否定义了 config_file_name
83
+
84
+ Args:
85
+ plugin_file: plugin.py 文件路径
86
+ plugin_name: 插件名称
87
+
88
+ Returns:
89
+ 是否定义了 config_file_name
90
+ """
91
+ try:
92
+ parser = CodeParser.from_file(plugin_file)
93
+ return parser.has_class_attribute(
94
+ attribute_name="config_file_name",
95
+ base_class="BasePlugin"
96
+ )
97
+ except Exception:
98
+ return False
99
+
100
+ def _extract_config_schema(self, plugin_file, plugin_name: str) -> dict | None:
101
+ """从 plugin.py 中提取 config_schema 定义
102
+
103
+ Args:
104
+ plugin_file: plugin.py 文件路径
105
+ plugin_name: 插件名称
106
+
107
+ Returns:
108
+ config_schema 字典,如果未定义返回 None
109
+ """
110
+ try:
111
+ parser = CodeParser.from_file(plugin_file)
112
+ config_schema = parser.find_class_attribute(
113
+ base_class="BasePlugin",
114
+ attribute_name="config_schema"
115
+ )
116
+ return config_schema
117
+ except Exception as e:
118
+ self.result.add_error(f"解析 plugin.py 失败: {e}")
119
+ return None
@@ -0,0 +1,107 @@
1
+ """
2
+ 插件元数据验证器
3
+ """
4
+
5
+ from ..utils.code_parser import CodeParser
6
+ from .base import BaseValidator, ValidationResult
7
+
8
+
9
+ class MetadataValidator(BaseValidator):
10
+ """插件元数据验证器
11
+
12
+ 检查 plugin.py 中的 PluginMetadata 是否完整
13
+ """
14
+
15
+ # 必需的元数据字段
16
+ REQUIRED_FIELDS = ["name", "description", "usage"]
17
+
18
+ # 推荐的元数据字段
19
+ RECOMMENDED_FIELDS = ["version", "author", "license"]
20
+
21
+ def validate(self) -> ValidationResult:
22
+ """执行元数据验证
23
+
24
+ Returns:
25
+ ValidationResult: 验证结果
26
+ """
27
+ # 获取插件名称
28
+ plugin_name = self._get_plugin_name()
29
+ if not plugin_name:
30
+ self.result.add_error("无法确定插件名称")
31
+ return self.result
32
+
33
+ # 元数据在 __init__.py 中
34
+ init_file = self.plugin_path / "__init__.py"
35
+ if not init_file.exists():
36
+ self.result.add_error(
37
+ "__init__.py 文件不存在",
38
+ suggestion="请创建 __init__.py 文件并定义 __plugin_meta__",
39
+ )
40
+ return self.result
41
+
42
+ # 使用 CodeParser 解析
43
+ try:
44
+ parser = CodeParser.from_file(init_file)
45
+ except SyntaxError as e:
46
+ self.result.add_error(
47
+ f"__init__.py 存在语法错误: {e.msg}",
48
+ file_path="__init__.py",
49
+ line_number=e.lineno if hasattr(e, "lineno") else None,
50
+ )
51
+ return self.result
52
+ except Exception as e:
53
+ self.result.add_error(f"读取 __init__.py 失败: {e}")
54
+ return self.result
55
+
56
+ # 查找 __plugin_meta__ 赋值
57
+ metadata_values = parser.find_assignments("__plugin_meta__")
58
+
59
+ if not metadata_values:
60
+ self.result.add_error(
61
+ "未找到 __plugin_meta__ 变量或 PluginMetadata 实例",
62
+ file_path="__init__.py",
63
+ suggestion="请在 __init__.py 中定义: __plugin_meta__ = PluginMetadata(...) | 可运行 'mpdt check --fix' 自动修复",
64
+ )
65
+ return self.result
66
+
67
+ # 使用增强的 CodeParser 解析 PluginMetadata 的参数
68
+ metadata_args = parser.find_call_arguments("__plugin_meta__", "PluginMetadata")
69
+
70
+ if metadata_args is None:
71
+ self.result.add_error(
72
+ "未找到 __plugin_meta__ 的 PluginMetadata 调用",
73
+ file_path="__init__.py",
74
+ suggestion="请使用 PluginMetadata(...) 构造 __plugin_meta__ | 可运行 'mpdt check --fix' 自动修复",
75
+ )
76
+ return self.result
77
+
78
+ self.result.add_info("找到 __plugin_meta__ 定义")
79
+
80
+ # 检查必需字段
81
+ missing_required = parser.get_missing_call_arguments("__plugin_meta__", self.REQUIRED_FIELDS, "PluginMetadata")
82
+
83
+ if missing_required:
84
+ for field in missing_required:
85
+ self.result.add_error(
86
+ f"PluginMetadata 缺少必需字段: {field}",
87
+ file_path="__init__.py",
88
+ suggestion=f'请在 PluginMetadata 中添加 {field}="..." 参数 | 可运行 \'mpdt check --fix\' 自动修复',
89
+ )
90
+ else:
91
+ self.result.add_info("所有必需的元数据字段都已提供")
92
+
93
+ # 检查推荐字段
94
+ missing_recommended = []
95
+ for field in self.RECOMMENDED_FIELDS:
96
+ if field not in metadata_args or not metadata_args[field]:
97
+ missing_recommended.append(field)
98
+
99
+ if missing_recommended:
100
+ fields_str = ", ".join(f'{f}="..."' for f in missing_recommended)
101
+ self.result.add_warning(
102
+ f"建议添加以下元数据字段: {', '.join(missing_recommended)}",
103
+ file_path="__init__.py",
104
+ suggestion=f"在 PluginMetadata 中添加: {fields_str}",
105
+ )
106
+
107
+ return self.result
@@ -0,0 +1,72 @@
1
+ """
2
+ 插件结构验证器
3
+ """
4
+
5
+ from .base import BaseValidator, ValidationResult
6
+
7
+
8
+ class StructureValidator(BaseValidator):
9
+ """插件结构验证器
10
+
11
+ 检查插件的目录结构和必需文件
12
+ """
13
+
14
+ # 必需的文件
15
+ REQUIRED_FILES = ["__init__.py", "plugin.py"]
16
+
17
+ # 推荐的文件
18
+ RECOMMENDED_FILES = ["README.md"]
19
+
20
+ # 推荐的目录
21
+ RECOMMENDED_DIRS = ["tests", "docs"]
22
+
23
+ def validate(self) -> ValidationResult:
24
+ """执行结构验证
25
+
26
+ Returns:
27
+ ValidationResult: 验证结果
28
+ """
29
+ # 获取插件名称
30
+ plugin_name = self._get_plugin_name()
31
+ if not plugin_name:
32
+ self.result.add_error(
33
+ "无法确定插件名称,请确保插件目录结构正确",
34
+ suggestion="插件应该有 plugin.py 文件",
35
+ )
36
+ return self.result
37
+
38
+ # 插件代码目录就是根目录
39
+ plugin_code_dir = self.plugin_path
40
+
41
+ # 检查必需的文件
42
+ for file_name in self.REQUIRED_FILES:
43
+ file_path = plugin_code_dir / file_name
44
+ if not file_path.exists():
45
+ self.result.add_error(
46
+ f"缺少必需文件: {file_name}",
47
+ file_path=str(file_path.relative_to(self.plugin_path)),
48
+ )
49
+
50
+ # 检查推荐的文件
51
+ for file_name in self.RECOMMENDED_FILES:
52
+ file_path = self.plugin_path / file_name
53
+ parent_file_path = self.plugin_path.parent / file_name
54
+ if not file_path.exists() and not parent_file_path.exists():
55
+ self.result.add_warning(
56
+ f"缺少推荐文件: {file_name}",
57
+ file_path=file_name,
58
+ suggestion=f"建议添加 {file_name} 以提供更好的文档或依赖管理",
59
+ )
60
+
61
+ # 检查推荐的目录
62
+ for dir_name in self.RECOMMENDED_DIRS:
63
+ dir_path = self.plugin_path / dir_name
64
+ parent_dir_path = self.plugin_path.parent / dir_name
65
+ if not dir_path.exists() and not parent_dir_path.exists():
66
+ self.result.add_warning(
67
+ f"缺少推荐目录: {dir_name}/",
68
+ file_path=dir_name,
69
+ suggestion=f"建议添加 {dir_name}/ 目录",
70
+ )
71
+
72
+ return self.result
@@ -0,0 +1,117 @@
1
+ """代码风格验证器
2
+
3
+ 使用 ruff 检查代码风格问题
4
+ """
5
+
6
+ import json
7
+ import subprocess
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .base import BaseValidator, ValidationResult
12
+
13
+
14
+ class StyleValidator(BaseValidator):
15
+ """代码风格验证器
16
+
17
+ 使用 ruff 检查代码风格和代码质量问题
18
+ """
19
+
20
+ def __init__(self, plugin_path: Path):
21
+ super().__init__(plugin_path)
22
+
23
+ def validate(self) -> ValidationResult:
24
+ """执行代码风格检查"""
25
+ result = ValidationResult(
26
+ validator_name="StyleValidator",
27
+ success=True
28
+ )
29
+
30
+ plugin_name = self._get_plugin_name()
31
+ if not plugin_name:
32
+ result.add_error("无法确定插件名称")
33
+ return result
34
+
35
+ # 检查 ruff 是否安装
36
+ if not self._is_ruff_installed():
37
+ result.add_warning("未安装 ruff,跳过代码风格检查", suggestion="运行 'pip install ruff' 安装")
38
+ return result
39
+
40
+ # 运行 ruff check
41
+ issues = self._run_ruff_check()
42
+
43
+ if issues:
44
+ for issue in issues:
45
+ result.add_warning(
46
+ issue["message"],
47
+ file_path=issue.get("file"),
48
+ line_number=issue.get("line"),
49
+ suggestion=issue.get("suggestion"),
50
+ )
51
+ else:
52
+ result.add_info("代码风格检查通过,未发现问题")
53
+
54
+ return result
55
+
56
+ def _is_ruff_installed(self) -> bool:
57
+ """检查 ruff 是否安装"""
58
+ try:
59
+ subprocess.run(["ruff", "--version"], capture_output=True, check=True, encoding='utf-8', errors='ignore')
60
+ return True
61
+ except (subprocess.CalledProcessError, FileNotFoundError):
62
+ return False
63
+
64
+ def _run_ruff_check(self) -> list[dict[str, Any]]:
65
+ """运行 ruff 检查
66
+
67
+ Returns:
68
+ 问题列表
69
+ """
70
+ issues = []
71
+
72
+ try:
73
+ # 构建命令
74
+ cmd = ["ruff", "check", "--output-format", "json", str(self.plugin_path)]
75
+
76
+ # 运行 ruff
77
+ result = subprocess.run(
78
+ cmd,
79
+ capture_output=True,
80
+ text=True,
81
+ encoding='utf-8',
82
+ errors='ignore'
83
+ )
84
+
85
+ # 解析输出
86
+ if result.stdout.strip():
87
+ try:
88
+ ruff_output = json.loads(result.stdout)
89
+ for item in ruff_output:
90
+ issues.append(
91
+ {
92
+ "file": str(Path(item["filename"]).relative_to(self.plugin_path)),
93
+ "line": item["location"]["row"],
94
+ "message": f"{item['code']}: {item['message']}",
95
+ "suggestion": self._get_fix_suggestion(item),
96
+ }
97
+ )
98
+ except json.JSONDecodeError:
99
+ # 如果不是 JSON 格式,尝试解析纯文本
100
+ pass
101
+
102
+ except Exception as e:
103
+ # 不抛出异常,只记录问题
104
+ issues.append({
105
+ "file": None,
106
+ "line": None,
107
+ "message": f"运行 ruff 时出错: {e}",
108
+ "suggestion": None
109
+ })
110
+
111
+ return issues
112
+
113
+ def _get_fix_suggestion(self, item: dict) -> str | None:
114
+ """获取修复建议"""
115
+ if item.get("fix"):
116
+ return "可自动修复,使用 --fix 选项"
117
+ return None
@@ -0,0 +1,206 @@
1
+ """类型检查验证器
2
+
3
+ 使用 mypy 进行类型检查
4
+ """
5
+
6
+ import re
7
+ import subprocess
8
+ from pathlib import Path
9
+
10
+ from mpdt.utils.config_manager import MPDTConfig
11
+
12
+ from .base import BaseValidator, ValidationResult
13
+
14
+
15
+ class TypeValidator(BaseValidator):
16
+ """类型检查验证器
17
+
18
+ 使用 mypy 进行静态类型检查
19
+ """
20
+
21
+ def __init__(self, plugin_path: Path):
22
+ super().__init__(plugin_path)
23
+ # 尝试找到 MoFox 主项目路径
24
+ self.MoFox_root = MPDTConfig().mofox_path
25
+
26
+ def validate(self) -> ValidationResult:
27
+ """执行类型检查"""
28
+ result = ValidationResult(
29
+ validator_name="TypeValidator",
30
+ success=True
31
+ )
32
+
33
+ plugin_name = self._get_plugin_name()
34
+ if not plugin_name:
35
+ result.add_error("无法确定插件名称")
36
+ return result
37
+
38
+ # 检查 mypy 是否安装
39
+ if not self._is_mypy_installed():
40
+ result.add_warning(
41
+ "未安装 mypy,跳过类型检查",
42
+ suggestion="运行 'pip install mypy' 安装"
43
+ )
44
+ return result
45
+
46
+ # 运行 mypy
47
+ issues = self._run_mypy_check()
48
+
49
+ if issues:
50
+ for issue in issues:
51
+ # 根据严重程度决定是错误还是警告
52
+ if issue.get("severity") == "error":
53
+ result.add_error(
54
+ issue["message"],
55
+ file_path=issue.get("file"),
56
+ line_number=issue.get("line"),
57
+ suggestion=issue.get("suggestion")
58
+ )
59
+ else:
60
+ result.add_warning(
61
+ issue["message"],
62
+ file_path=issue.get("file"),
63
+ line_number=issue.get("line"),
64
+ suggestion=issue.get("suggestion")
65
+ )
66
+
67
+ return result
68
+
69
+ def _is_mypy_installed(self) -> bool:
70
+ """检查 mypy 是否安装"""
71
+ try:
72
+ subprocess.run(
73
+ ["mypy", "--version"],
74
+ capture_output=True,
75
+ check=True,
76
+ encoding='utf-8',
77
+ errors='ignore'
78
+ )
79
+ return True
80
+ except (subprocess.CalledProcessError, FileNotFoundError):
81
+ return False
82
+
83
+ return None
84
+
85
+ def _run_mypy_check(self) -> list[dict]:
86
+ """运行 mypy 检查
87
+
88
+ Returns:
89
+ 问题列表
90
+ """
91
+ issues = []
92
+
93
+ try:
94
+ # 构建命令
95
+ cmd = [
96
+ "mypy",
97
+ str(self.plugin_path),
98
+ "--no-error-summary",
99
+ "--show-column-numbers",
100
+ "--show-error-codes",
101
+ "--no-namespace-packages", # 避免包命名空间问题
102
+ ]
103
+
104
+ # 如果找到了 MoFox-Bot 主项目,添加到 Python 路径
105
+ if self.MoFox_root:
106
+ cmd.extend(["--python-path", str(self.MoFox_root)])
107
+
108
+ # 运行 mypy
109
+ result = subprocess.run(
110
+ cmd,
111
+ capture_output=True,
112
+ text=True,
113
+ encoding='utf-8',
114
+ errors='ignore'
115
+ )
116
+
117
+ # 解析输出
118
+ if result.stdout.strip():
119
+ for line in result.stdout.strip().split('\n'):
120
+ issue = self._parse_mypy_line(line)
121
+ if issue:
122
+ # 如果找不到 MoFox 根目录,过滤掉所有 src.* 模块的导入错误
123
+ if not self.MoFox_root:
124
+ # 检查是否是 src.* 模块的导入错误
125
+ if "Cannot find implementation" in issue["message"] and '"src.' in issue["message"]:
126
+ continue # 跳过这个错误
127
+ # 检查是否是因为基类是 Any 导致的错误(因为导入失败)
128
+ if "has type \"Any\"" in issue["message"]:
129
+ continue # 跳过这个错误
130
+ issues.append(issue)
131
+
132
+ except Exception as e:
133
+ issues.append({
134
+ "file": None,
135
+ "line": None,
136
+ "message": f"运行 mypy 时出错: {e}",
137
+ "severity": "error",
138
+ "suggestion": None
139
+ })
140
+
141
+ return issues
142
+
143
+ def _parse_mypy_line(self, line: str) -> dict | None:
144
+ """解析 mypy 输出的一行
145
+
146
+ 格式: file.py:123:45: error: Message [error-code]
147
+
148
+ Args:
149
+ line: mypy 输出的一行
150
+
151
+ Returns:
152
+ 解析后的问题字典,如果解析失败返回 None
153
+ """
154
+ # 匹配 mypy 输出格式
155
+ pattern = r'^(.+?):(\d+):(?:\d+:)?\s+(error|warning|note):\s+(.+?)(?:\s+\[(.+?)\])?$'
156
+ match = re.match(pattern, line)
157
+
158
+ if not match:
159
+ return None
160
+
161
+ file_path, line_num, severity, message, error_code = match.groups()
162
+
163
+ try:
164
+ # 转换为相对路径
165
+ rel_path = str(Path(file_path).relative_to(self.plugin_path))
166
+ except ValueError:
167
+ rel_path = file_path
168
+
169
+ issue = {
170
+ "file": rel_path,
171
+ "line": int(line_num),
172
+ "message": message.strip(),
173
+ "severity": severity,
174
+ "suggestion": None
175
+ }
176
+
177
+ # 添加错误代码到消息中
178
+ if error_code:
179
+ issue["message"] = f"[{error_code}] {issue['message']}"
180
+
181
+ # 根据常见错误提供建议
182
+ issue["suggestion"] = self._get_type_hint_suggestion(message, error_code)
183
+
184
+ return issue
185
+
186
+ def _get_type_hint_suggestion(self, message: str, error_code: str | None) -> str | None:
187
+ """根据错误消息提供类型提示建议"""
188
+ if not error_code:
189
+ return None
190
+
191
+ suggestions = {
192
+ "no-untyped-def": "为函数添加类型注解",
193
+ "no-untyped-call": "被调用的函数缺少类型注解",
194
+ "assignment": "检查赋值的类型是否匹配",
195
+ "return-value": "检查返回值类型是否与声明一致",
196
+ "arg-type": "检查参数类型是否正确",
197
+ "attr-defined": "检查属性是否存在",
198
+ "name-defined": "检查名称是否定义",
199
+ "import": "检查导入是否正确"
200
+ }
201
+
202
+ for code, suggestion in suggestions.items():
203
+ if code in error_code:
204
+ return suggestion
205
+
206
+ return None