feat(ci): add mypy error ratchet gate

This commit is contained in:
jxxghp
2026-08-25 06:59:18 +08:00
parent 2742960bfb
commit 17a23c47df
5 changed files with 1115 additions and 0 deletions
+3
View File
@@ -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
+6
View File
@@ -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/)
+104
View File
@@ -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
View File
@@ -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
}
}
+59
View File
@@ -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)