mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
feat(ci): add mypy error ratchet gate
This commit is contained in:
@@ -65,6 +65,9 @@ jobs:
|
||||
- name: Check process runtime service locators
|
||||
run: uv run --locked --no-sync python scripts/architecture/service_locator.py
|
||||
|
||||
- name: Check mypy error ratchet
|
||||
run: uv run --locked --no-sync python scripts/architecture/mypy_ratchet.py
|
||||
|
||||
- name: Check startup performance contract
|
||||
run: >-
|
||||
uv run --locked --no-sync python
|
||||
|
||||
@@ -210,12 +210,18 @@ uvx --from pip-audit==2.10.1 pip-audit \
|
||||
--check-plugins --plugin-repo ../MoviePilot-Plugins \
|
||||
--report official-plugin-architecture-report.json
|
||||
uv run --locked --no-sync pylint app/
|
||||
uv run --locked --no-sync python scripts/architecture/mypy_ratchet.py
|
||||
```
|
||||
|
||||
GitHub Actions 会在 `v3` 的 PR/push 中独立执行宿主架构门禁,并对本次改动的 Python
|
||||
文件执行 Pylint 硬门禁;`app/` 全量结果作为建议性报告上传。最新官方插件仓通过每周
|
||||
或手工观察工作流检查,只上传语义差异报告,不会自动更新已提交基线。
|
||||
|
||||
mypy 类型错误基线(`tests/fixtures/architecture/mypy-baseline.json`)只降不增:
|
||||
新增文件、错误码或既有计数增长都会被 `scripts/architecture/mypy_ratchet.py` 拒绝;
|
||||
修复存量错误后用同脚本 `--write` 收紧基线,禁止为绕过门禁放宽基线。受治文件清单
|
||||
仍由 `mypy.ini` 的 `files=` 维护并保持零错误。
|
||||
|
||||
### 7. 参考资源
|
||||
|
||||
- [uv 官方文档](https://docs.astral.sh/uv/)
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""为宿主源码维护 mypy 类型错误只降不增的基线。
|
||||
|
||||
与 ``complexity.py`` 同一模式:AST/subprocess 收集当前计数,与 JSON 基线对比,
|
||||
删除和减少均合法,新增文件或错误码、既有计数增长都会被拒绝。
|
||||
基线按 文件 -> 错误码 -> 数量 三级组织,修复单个文件后可用 ``--write`` 收紧基线,
|
||||
不影响其他文件的存量豁免。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[2]
|
||||
DEFAULT_BASELINE = PROJECT_ROOT / "tests/fixtures/architecture/mypy-baseline.json"
|
||||
|
||||
# 只扫描宿主源码;app/plugins 是运行时插件副本,质量由插件市场链路自行管理。
|
||||
MYPY_TARGETS = ("app",)
|
||||
MYPY_EXCLUDES = ("app/plugins",)
|
||||
|
||||
# 形如 app/foo.py:12: error: 消息说明 [error-code];个别错误可能缺代码。
|
||||
_ERROR_LINE = re.compile(r"^(?P<path>.+?):\d+(?::\d+)?: error: .+?(?:\s+\[(?P<code>[a-z0-9-]+)\])?$")
|
||||
|
||||
|
||||
def run_mypy() -> str:
|
||||
"""以当前解释器运行全量 mypy 并返回 stdout(非零退出码属于预期结果)。"""
|
||||
command = [sys.executable, "-m", "mypy", *MYPY_TARGETS]
|
||||
for pattern in MYPY_EXCLUDES:
|
||||
command += ["--exclude", pattern]
|
||||
result = subprocess.run(
|
||||
command,
|
||||
cwd=PROJECT_ROOT,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
return result.stdout
|
||||
|
||||
|
||||
def parse_errors(output: str) -> dict[str, dict[str, int]]:
|
||||
"""把 mypy 输出聚合为 文件 -> 错误码 -> 数量 的精确计数。"""
|
||||
report: dict[str, dict[str, int]] = defaultdict(lambda: defaultdict(int))
|
||||
for raw_line in output.splitlines():
|
||||
line = raw_line.strip()
|
||||
# 排除摘要行与源码上下文片段,只匹配真实错误行。
|
||||
if ": error:" not in line:
|
||||
continue
|
||||
match = _ERROR_LINE.match(line)
|
||||
if not match:
|
||||
continue
|
||||
path = match.group("path").replace("\\", "/")
|
||||
code = match.group("code") or "unknown"
|
||||
report[path][code] += 1
|
||||
return {path: dict(codes) for path, codes in sorted(report.items())}
|
||||
|
||||
|
||||
def compare_counts(
|
||||
baseline: dict[str, dict[str, int]], current: dict[str, dict[str, int]]
|
||||
) -> list[str]:
|
||||
"""返回新增文件/错误码或既有计数增长问题;删除与减少均合法。"""
|
||||
problems = []
|
||||
for path, codes in current.items():
|
||||
previous = baseline.get(path, {})
|
||||
for code, count in codes.items():
|
||||
if code not in previous:
|
||||
problems.append(f"{path}: 新增类型错误 [{code}] x{count}")
|
||||
elif count > previous[code]:
|
||||
problems.append(f"{path}: 既有错误增长 [{code}] {previous[code]}->{count}")
|
||||
return problems
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""执行 mypy baseline check 或显式 write。"""
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--write", action="store_true", help="写入当前类型错误基线")
|
||||
parser.add_argument("--baseline", type=Path, default=DEFAULT_BASELINE)
|
||||
args = parser.parse_args()
|
||||
current = parse_errors(run_mypy())
|
||||
if args.write:
|
||||
args.baseline.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.baseline.write_text(
|
||||
json.dumps(current, ensure_ascii=False, indent=2, sort_keys=True) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
print(f"已写入 {args.baseline.relative_to(PROJECT_ROOT)}")
|
||||
return 0
|
||||
baseline = json.loads(args.baseline.read_text(encoding="utf-8"))
|
||||
problems = compare_counts(baseline, current)
|
||||
if problems:
|
||||
print("\n".join(problems))
|
||||
print("提示:修复后可用 --write 收紧基线;禁止为绕过门禁放宽基线。")
|
||||
return 1
|
||||
total = sum(count for codes in current.values() for count in codes.values())
|
||||
print(f"mypy ratchet 通过(存量 {total} 个类型错误,只降不增)")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+943
@@ -0,0 +1,943 @@
|
||||
{
|
||||
"app/adapters/cache/backends.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/adapters/external/cookiecloud.py": {
|
||||
"unknown": 9
|
||||
},
|
||||
"app/adapters/external/location.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/adapters/external/market.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/adapters/external/ocr.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/adapters/external/wechat_crypt.py": {
|
||||
"unknown": 29
|
||||
},
|
||||
"app/adapters/network/browser.py": {
|
||||
"unknown": 24
|
||||
},
|
||||
"app/adapters/network/cloudflare.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/adapters/network/http.py": {
|
||||
"unknown": 142
|
||||
},
|
||||
"app/adapters/network/ip.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/adapters/system/backup/database.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/adapters/system/fsproxy.py": {
|
||||
"unknown": 33
|
||||
},
|
||||
"app/adapters/system/fsworker.py": {
|
||||
"unknown": 13
|
||||
},
|
||||
"app/adapters/system/host.py": {
|
||||
"misc": 1,
|
||||
"operator": 1,
|
||||
"unknown": 23
|
||||
},
|
||||
"app/adapters/system/resource.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/adapters/system/rust.py": {
|
||||
"unknown": 28
|
||||
},
|
||||
"app/adapters/system/stdio.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/adapters/system/update.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/adapters/web/correlation.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/adapters/web/plugin/routes.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/adapters/web/security/access.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/agent/contracts.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/agent/llm/capability.py": {
|
||||
"unknown": 12
|
||||
},
|
||||
"app/agent/llm/helper.py": {
|
||||
"unknown": 32
|
||||
},
|
||||
"app/agent/llm/provider.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/agent/middleware/activity_log.py": {
|
||||
"unknown": 9
|
||||
},
|
||||
"app/agent/middleware/jobs.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/agent/middleware/memory.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/agent/middleware/patch_tool_calls.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/agent/middleware/skills.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/agent/middleware/summarization.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/agent/middleware/tool_selection.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/agent/middleware/usage.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/agent/policy/orchestrator.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/agent/policy/sanitizer.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/agent/prompt/__init__.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/agent/runtime.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/agent/skills/metadata.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/agent/skills/registry.py": {
|
||||
"unknown": 44
|
||||
},
|
||||
"app/agent/tools/catalog.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/agent/tools/impl/_terminal_session.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/agent/tools/impl/_torrent_search_utils.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/agent/tools/impl/create_agent_task.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/agent/tools/impl/update_agent_task.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/api/endpoints/agent.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/api/endpoints/plugin.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/api/endpoints/system.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/api/response.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/api/servcookie.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/application/agent.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/application/agentdata.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/application/agenttask.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/application/commands.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/application/configuration.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/application/dashboard.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/application/directory.py": {
|
||||
"unknown": 13
|
||||
},
|
||||
"app/application/download/tasks.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/application/downloader.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/application/formatting.py": {
|
||||
"unknown": 26
|
||||
},
|
||||
"app/application/history.py": {
|
||||
"unknown": 9
|
||||
},
|
||||
"app/application/maintenance.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/application/mediaserver.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/application/messaging/agent.py": {
|
||||
"unknown": 20
|
||||
},
|
||||
"app/application/messaging/ingress.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/application/messaging/interaction.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/application/messaging/media.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/application/messaging/message.py": {
|
||||
"arg-type": 5,
|
||||
"assignment": 3,
|
||||
"dict-item": 1,
|
||||
"misc": 1,
|
||||
"no-any-return": 4,
|
||||
"no-untyped-call": 2,
|
||||
"no-untyped-def": 11,
|
||||
"type-arg": 18,
|
||||
"union-attr": 2,
|
||||
"var-annotated": 1
|
||||
},
|
||||
"app/application/messaging/plugin.py": {
|
||||
"unknown": 9
|
||||
},
|
||||
"app/application/messaging/site.py": {
|
||||
"unknown": 7
|
||||
},
|
||||
"app/application/messaging/skill.py": {
|
||||
"unknown": 14
|
||||
},
|
||||
"app/application/messaging/subscribe.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/application/module.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/application/music/catalog.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/application/notification.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/application/plugin/catalog.py": {
|
||||
"unknown": 11
|
||||
},
|
||||
"app/application/plugin/config.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/application/plugin/install.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/application/plugin/lifecycle.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/application/rss.py": {
|
||||
"unknown": 21
|
||||
},
|
||||
"app/application/rules.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/application/security/cookie.py": {
|
||||
"unknown": 25
|
||||
},
|
||||
"app/application/security/otp.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/application/security/passkeys.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/application/security/token.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/application/security/twofactor.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/application/security/url.py": {
|
||||
"unknown": 15
|
||||
},
|
||||
"app/application/servarr.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/application/server/report.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/application/server/share.py": {
|
||||
"unknown": 7
|
||||
},
|
||||
"app/application/service.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/application/site/mutation.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/application/site/query.py": {
|
||||
"unknown": 7
|
||||
},
|
||||
"app/application/site/sites.pyi": {
|
||||
"unknown": 7
|
||||
},
|
||||
"app/application/storage.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/application/subscription/complete.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/application/subscription/contract.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/application/subscription/query.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/application/subscription/write.py": {
|
||||
"unknown": 22
|
||||
},
|
||||
"app/application/torrent.py": {
|
||||
"unknown": 39
|
||||
},
|
||||
"app/application/torrent_cache.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/application/transfer.py": {
|
||||
"unknown": 104
|
||||
},
|
||||
"app/chain/_interaction.py": {
|
||||
"unknown": 12
|
||||
},
|
||||
"app/db/models/agentchat.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/db/models/agenttask.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/db/models/downloadfailure.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/db/models/downloadhistory.py": {
|
||||
"unknown": 19
|
||||
},
|
||||
"app/db/models/mediaserver.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/db/models/message.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/db/models/passkey.py": {
|
||||
"unknown": 12
|
||||
},
|
||||
"app/db/models/plugindata.py": {
|
||||
"unknown": 8
|
||||
},
|
||||
"app/db/models/site.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/db/models/siteicon.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/db/models/sitestatistic.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/db/models/siteuserdata.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/db/models/subscribe.py": {
|
||||
"unknown": 26
|
||||
},
|
||||
"app/db/models/subscribehistory.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/db/models/systemconfig.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/db/models/transferhistory.py": {
|
||||
"unknown": 24
|
||||
},
|
||||
"app/db/models/transferpending.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/db/models/user.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/db/models/userconfig.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/db/models/workflow.py": {
|
||||
"unknown": 31
|
||||
},
|
||||
"app/doctor/checks.py": {
|
||||
"unknown": 7
|
||||
},
|
||||
"app/doctor/runner.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/domain/context.py": {
|
||||
"unknown": 225
|
||||
},
|
||||
"app/domain/meta/customization.py": {
|
||||
"unknown": 8
|
||||
},
|
||||
"app/domain/meta/metaanime.py": {
|
||||
"unknown": 11
|
||||
},
|
||||
"app/domain/meta/metabase.py": {
|
||||
"unknown": 19
|
||||
},
|
||||
"app/domain/meta/metamusic.py": {
|
||||
"unknown": 16
|
||||
},
|
||||
"app/domain/meta/metavideo.py": {
|
||||
"unknown": 33
|
||||
},
|
||||
"app/domain/meta/releasegroup.py": {
|
||||
"unknown": 7
|
||||
},
|
||||
"app/domain/meta/runtime.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/domain/meta/streamingplatform.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/domain/meta/words.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/domain/metainfo.py": {
|
||||
"unknown": 24
|
||||
},
|
||||
"app/domain/scraper.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/domain/title.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/domain/tokens.py": {
|
||||
"unknown": 9
|
||||
},
|
||||
"app/foundation/crypto.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/foundation/dom.py": {
|
||||
"unknown": 7
|
||||
},
|
||||
"app/foundation/environment.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/foundation/reflection.py": {
|
||||
"unknown": 12
|
||||
},
|
||||
"app/foundation/singleton.py": {
|
||||
"unknown": 14
|
||||
},
|
||||
"app/foundation/temporal.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/foundation/text.py": {
|
||||
"unknown": 13
|
||||
},
|
||||
"app/foundation/url.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/modules/anilist/anilist.py": {
|
||||
"unknown": 60
|
||||
},
|
||||
"app/modules/bangumi/bangumi.py": {
|
||||
"unknown": 26
|
||||
},
|
||||
"app/modules/dingtalk/dingtalk.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/modules/discord/discord.py": {
|
||||
"unknown": 48
|
||||
},
|
||||
"app/modules/douban/apiv2.py": {
|
||||
"unknown": 170
|
||||
},
|
||||
"app/modules/douban/scraper.py": {
|
||||
"unknown": 7
|
||||
},
|
||||
"app/modules/emby/emby.py": {
|
||||
"unknown": 37
|
||||
},
|
||||
"app/modules/feishu/feishu.py": {
|
||||
"unknown": 47
|
||||
},
|
||||
"app/modules/imdb/api.py": {
|
||||
"unknown": 27
|
||||
},
|
||||
"app/modules/indexer/parser/__init__.py": {
|
||||
"unknown": 53
|
||||
},
|
||||
"app/modules/indexer/parser/bitpt.py": {
|
||||
"unknown": 45
|
||||
},
|
||||
"app/modules/indexer/parser/discuz.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/modules/indexer/parser/file_list.py": {
|
||||
"unknown": 14
|
||||
},
|
||||
"app/modules/indexer/parser/gazelle.py": {
|
||||
"unknown": 13
|
||||
},
|
||||
"app/modules/indexer/parser/ipt_project.py": {
|
||||
"unknown": 13
|
||||
},
|
||||
"app/modules/indexer/parser/mtorrent.py": {
|
||||
"unknown": 21
|
||||
},
|
||||
"app/modules/indexer/parser/nexus_php.py": {
|
||||
"unknown": 31
|
||||
},
|
||||
"app/modules/indexer/parser/nexus_rabbit.py": {
|
||||
"unknown": 19
|
||||
},
|
||||
"app/modules/indexer/parser/rousi.py": {
|
||||
"unknown": 23
|
||||
},
|
||||
"app/modules/indexer/parser/small_horse.py": {
|
||||
"unknown": 16
|
||||
},
|
||||
"app/modules/indexer/parser/sunnypt.py": {
|
||||
"unknown": 18
|
||||
},
|
||||
"app/modules/indexer/parser/tnode.py": {
|
||||
"unknown": 12
|
||||
},
|
||||
"app/modules/indexer/parser/torrent_leech.py": {
|
||||
"unknown": 11
|
||||
},
|
||||
"app/modules/indexer/parser/unit3d.py": {
|
||||
"unknown": 12
|
||||
},
|
||||
"app/modules/indexer/parser/yema.py": {
|
||||
"unknown": 18
|
||||
},
|
||||
"app/modules/indexer/parser/zhixing.py": {
|
||||
"unknown": 47
|
||||
},
|
||||
"app/modules/indexer/spider/__init__.py": {
|
||||
"unknown": 93
|
||||
},
|
||||
"app/modules/indexer/spider/haidan.py": {
|
||||
"unknown": 19
|
||||
},
|
||||
"app/modules/indexer/spider/hddolby.py": {
|
||||
"unknown": 21
|
||||
},
|
||||
"app/modules/indexer/spider/mtorrent.py": {
|
||||
"unknown": 20
|
||||
},
|
||||
"app/modules/indexer/spider/rousi.py": {
|
||||
"unknown": 24
|
||||
},
|
||||
"app/modules/indexer/spider/sunnypt.py": {
|
||||
"unknown": 31
|
||||
},
|
||||
"app/modules/indexer/spider/tnode.py": {
|
||||
"unknown": 23
|
||||
},
|
||||
"app/modules/indexer/spider/torrentleech.py": {
|
||||
"unknown": 31
|
||||
},
|
||||
"app/modules/indexer/spider/yema.py": {
|
||||
"unknown": 21
|
||||
},
|
||||
"app/modules/jellyfin/jellyfin.py": {
|
||||
"return": 1,
|
||||
"unknown": 47
|
||||
},
|
||||
"app/modules/musicbrainz/music_cache.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/modules/navidrome/navidrome.py": {
|
||||
"unknown": 12
|
||||
},
|
||||
"app/modules/plex/plex.py": {
|
||||
"unknown": 34
|
||||
},
|
||||
"app/modules/qbittorrent/qbittorrent.py": {
|
||||
"unknown": 42
|
||||
},
|
||||
"app/modules/qqbot/api.py": {
|
||||
"unknown": 17
|
||||
},
|
||||
"app/modules/qqbot/gateway.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/modules/qqbot/qqbot.py": {
|
||||
"unknown": 16
|
||||
},
|
||||
"app/modules/rtorrent/rtorrent.py": {
|
||||
"unknown": 182
|
||||
},
|
||||
"app/modules/slack/slack.py": {
|
||||
"unknown": 31
|
||||
},
|
||||
"app/modules/synologychat/synologychat.py": {
|
||||
"unknown": 14
|
||||
},
|
||||
"app/modules/themoviedb/category.py": {
|
||||
"unknown": 13
|
||||
},
|
||||
"app/modules/themoviedb/tmdb_cache.py": {
|
||||
"unknown": 19
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/as_obj.py": {
|
||||
"unknown": 41
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/account.py": {
|
||||
"unknown": 59
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/auth.py": {
|
||||
"unknown": 13
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/certification.py": {
|
||||
"unknown": 8
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/change.py": {
|
||||
"unknown": 16
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/collection.py": {
|
||||
"unknown": 12
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/company.py": {
|
||||
"unknown": 16
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/configuration.py": {
|
||||
"unknown": 26
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/credit.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/discover.py": {
|
||||
"unknown": 9
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/episode.py": {
|
||||
"unknown": 40
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/find.py": {
|
||||
"unknown": 36
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/genre.py": {
|
||||
"unknown": 8
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/group.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/keyword.py": {
|
||||
"unknown": 8
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/list.py": {
|
||||
"unknown": 28
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/movie.py": {
|
||||
"unknown": 92
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/network.py": {
|
||||
"unknown": 12
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/person.py": {
|
||||
"unknown": 44
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/provider.py": {
|
||||
"unknown": 12
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/review.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/search.py": {
|
||||
"unknown": 29
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/season.py": {
|
||||
"unknown": 36
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/trending.py": {
|
||||
"unknown": 36
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/objs/tv.py": {
|
||||
"unknown": 104
|
||||
},
|
||||
"app/modules/themoviedb/tmdbv3api/tmdb.py": {
|
||||
"unknown": 87
|
||||
},
|
||||
"app/modules/thetvdb/tvdb_v4_official.py": {
|
||||
"unknown": 274
|
||||
},
|
||||
"app/modules/transmission/transmission.py": {
|
||||
"unknown": 41
|
||||
},
|
||||
"app/modules/trimemedia/api.py": {
|
||||
"unknown": 21
|
||||
},
|
||||
"app/modules/ugreen/api.py": {
|
||||
"unknown": 31
|
||||
},
|
||||
"app/modules/ugreen/crypto.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/modules/ugreen/ugreen.py": {
|
||||
"unknown": 45
|
||||
},
|
||||
"app/modules/vocechat/vocechat.py": {
|
||||
"unknown": 8
|
||||
},
|
||||
"app/modules/wechat/wechat.py": {
|
||||
"unknown": 26
|
||||
},
|
||||
"app/modules/wechat/wechatbot.py": {
|
||||
"unknown": 22
|
||||
},
|
||||
"app/modules/wechatclawbot/wechatclawbot.py": {
|
||||
"unknown": 15
|
||||
},
|
||||
"app/modules/zspace/zspace.py": {
|
||||
"unknown": 44
|
||||
},
|
||||
"app/monitor/snapshot.py": {
|
||||
"unknown": 7
|
||||
},
|
||||
"app/monitor/syslimits.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/monitor/watcher.py": {
|
||||
"unknown": 18
|
||||
},
|
||||
"app/runtime/cache.py": {
|
||||
"unknown": 89
|
||||
},
|
||||
"app/runtime/capabilities/runtime.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/runtime/compat/imports.py": {
|
||||
"unknown": 20
|
||||
},
|
||||
"app/runtime/config.py": {
|
||||
"unknown": 52
|
||||
},
|
||||
"app/runtime/debounce.py": {
|
||||
"unknown": 37
|
||||
},
|
||||
"app/runtime/deprecation/policy.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/runtime/event/binding.py": {
|
||||
"unknown": 7
|
||||
},
|
||||
"app/runtime/event/contracts.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/runtime/event/dispatch.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/runtime/event/registry.py": {
|
||||
"unknown": 14
|
||||
},
|
||||
"app/runtime/execution.py": {
|
||||
"unknown": 9
|
||||
},
|
||||
"app/runtime/extensions/host_module_adapter.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/runtime/extensions/module/dispatcher.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/runtime/extensions/plugin/access.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/runtime/extensions/plugin/admission.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/runtime/extensions/plugin/catalog.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/runtime/extensions/plugin/clone.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/runtime/extensions/plugin/lifecycle.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/runtime/extensions/plugin/metadata.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/runtime/extensions/plugin/monitor.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/runtime/extensions/plugin/paths.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/runtime/extensions/plugin/projection.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/runtime/extensions/plugin/storage.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/runtime/extensions/plugin/sync.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/runtime/extensions/plugin/system.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/runtime/extensions/service_config.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/runtime/gc.py": {
|
||||
"unknown": 7
|
||||
},
|
||||
"app/runtime/localization.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/runtime/managed_resources.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/runtime/progress.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/runtime/rate.py": {
|
||||
"unknown": 29
|
||||
},
|
||||
"app/runtime/scheduling.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/runtime/settings.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/runtime/thread.py": {
|
||||
"unused-ignore": 1
|
||||
},
|
||||
"app/scheduler.py": {
|
||||
"import-untyped": 1
|
||||
},
|
||||
"app/schemas/agent.py": {
|
||||
"unknown": 26
|
||||
},
|
||||
"app/schemas/cache.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/schemas/category.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/schemas/common.py": {
|
||||
"unknown": 13
|
||||
},
|
||||
"app/schemas/context.py": {
|
||||
"unknown": 18
|
||||
},
|
||||
"app/schemas/dashboard.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/schemas/download.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/schemas/event.py": {
|
||||
"unknown": 17
|
||||
},
|
||||
"app/schemas/file.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/schemas/history.py": {
|
||||
"unknown": 4
|
||||
},
|
||||
"app/schemas/llm.py": {
|
||||
"unknown": 9
|
||||
},
|
||||
"app/schemas/mcp.py": {
|
||||
"misc": 1,
|
||||
"unknown": 22
|
||||
},
|
||||
"app/schemas/media.py": {
|
||||
"unknown": 9
|
||||
},
|
||||
"app/schemas/mediaserver.py": {
|
||||
"unknown": 14
|
||||
},
|
||||
"app/schemas/message.py": {
|
||||
"unknown": 21
|
||||
},
|
||||
"app/schemas/mfa.py": {
|
||||
"misc": 1,
|
||||
"unknown": 4
|
||||
},
|
||||
"app/schemas/monitoring.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/schemas/music.py": {
|
||||
"unknown": 10
|
||||
},
|
||||
"app/schemas/notification.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/schemas/openai.py": {
|
||||
"unknown": 23
|
||||
},
|
||||
"app/schemas/plugin.py": {
|
||||
"unknown": 18
|
||||
},
|
||||
"app/schemas/response.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/schemas/rule.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/schemas/search.py": {
|
||||
"unknown": 2
|
||||
},
|
||||
"app/schemas/servarr.py": {
|
||||
"unknown": 18
|
||||
},
|
||||
"app/schemas/servcookie.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/schemas/site.py": {
|
||||
"unknown": 8
|
||||
},
|
||||
"app/schemas/storage.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/schemas/subscribe.py": {
|
||||
"unknown": 11
|
||||
},
|
||||
"app/schemas/system.py": {
|
||||
"unknown": 22
|
||||
},
|
||||
"app/schemas/tmdb.py": {
|
||||
"unknown": 5
|
||||
},
|
||||
"app/schemas/token.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/schemas/transfer.py": {
|
||||
"unknown": 21
|
||||
},
|
||||
"app/schemas/types.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/schemas/user.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/schemas/workflow.py": {
|
||||
"unknown": 19
|
||||
},
|
||||
"app/sdk/_legacy/transfer.py": {
|
||||
"unknown": 1
|
||||
},
|
||||
"app/sdk/string.py": {
|
||||
"unknown": 3
|
||||
},
|
||||
"app/testing/network_guard.py": {
|
||||
"unknown": 6
|
||||
},
|
||||
"app/testing/stub.py": {
|
||||
"unknown": 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
"""mypy 错误数只降不增 ratchet 的解析与对比逻辑测试。"""
|
||||
|
||||
from scripts.architecture.mypy_ratchet import compare_counts, parse_errors
|
||||
|
||||
|
||||
MYPY_SAMPLE = """
|
||||
app/application/a.py:12: error: Function is missing a type annotation [no-untyped-def]
|
||||
app/application/a.py:15: error: Function is missing a type annotation [no-untyped-def]
|
||||
def run(self):
|
||||
^
|
||||
app/application/a.py:20: error: Missing type parameters for generic type "dict" [type-arg]
|
||||
Found 3 errors in 1 file (checked 500 source files)
|
||||
"""
|
||||
|
||||
|
||||
def test_parse_errors_aggregates_per_file_and_code() -> None:
|
||||
"""错误行按文件与错误码聚合,源码上下文与摘要行不计入。"""
|
||||
report = parse_errors(MYPY_SAMPLE)
|
||||
|
||||
assert report == {
|
||||
"app/application/a.py": {
|
||||
"no-untyped-def": 2,
|
||||
"type-arg": 1,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_parse_errors_buckets_missing_code_as_unknown() -> None:
|
||||
"""缺少错误码的错误行归入 unknown 桶,不丢失计数。"""
|
||||
report = parse_errors("app/b.py:3: error: Something went wrong\n")
|
||||
|
||||
assert report == {"app/b.py": {"unknown": 1}}
|
||||
|
||||
|
||||
def test_ratchet_allows_removal_and_reduction() -> None:
|
||||
"""文件删除或错误数减少必须允许通过。"""
|
||||
baseline = {"app/a.py": {"arg-type": 5, "misc": 2}}
|
||||
current = {"app/a.py": {"arg-type": 3}}
|
||||
|
||||
assert compare_counts(baseline, current) == []
|
||||
|
||||
|
||||
def test_ratchet_rejects_growth_and_new_errors() -> None:
|
||||
"""既有错误码增长和新增文件/错误码必须同时给出精确诊断。"""
|
||||
baseline = {
|
||||
"app/a.py": {"arg-type": 5},
|
||||
"app/c.py": {"misc": 1},
|
||||
}
|
||||
current = {
|
||||
"app/a.py": {"arg-type": 6, "type-arg": 2},
|
||||
"app/b.py": {"no-untyped-def": 10},
|
||||
"app/c.py": {},
|
||||
}
|
||||
|
||||
problems = compare_counts(baseline, current)
|
||||
|
||||
assert any("app/a.py" in p and "arg-type" in p and "既有错误增长" in p for p in problems)
|
||||
assert any("app/a.py" in p and "type-arg" in p and "新增类型错误" in p for p in problems)
|
||||
assert any("app/b.py" in p and "新增类型错误" in p for p in problems)
|
||||
Reference in New Issue
Block a user