Skip to content

Commit a4e8727

Browse files
author
zhangtao
committed
chore(backend): 新增 basedpyright 校准配置,并修复它暴露的三处真缺陷
背景:仓库此前没有 pyright/basedpyright 配置,basedpyright 使用自身默认(≈recommended), 在 app/ 下产生 247 errors / 3799 warnings,其中 143 条是 reportMissingTypeArgument (未参数化泛型)这类注解完整度噪音,会把真缺陷淹没(例如 OSS 的 Client.uploader 方法误用)。 配置(backend/pyrightconfig.json) - typeCheckingMode: basic;显式把能抓真缺陷的规则保持为 error (reportAttributeAccessIssue / reportArgumentType / reportOptionalMemberAccess / reportIncompatibleMethodOverride / reportConstantRedefinition / reportMissingImports 等) - 关闭注解完整度与风格类规则;tests/ 用 executionEnvironments 单独放宽(extraPaths 保证 app 可解析) - 结果:app + main.py + tests 全量 0 errors - 红绿验证:合成「str | None 传参」「MethodType 上取属性」「Optional 成员访问」三类 仍全部报 error,证明校准没有把真缺陷一起关掉 修复配置暴露的三处真缺陷 1) template_safety.py:_SQLALCHEMY_TYPE_WHITELIST 对全大写常量二次赋值 → 合并为一次成型 2) template_safety.py:normalize_column_type 假定 _TYPE_BASE_RE 必命中(用 # type: ignore 盖住, None 时 .group() 会崩)→ 改为 None 安全,去掉 type-ignore 3) node/service.py ×2:CustomException(msg=reason) 中 reason 为 str | None,客户端可能收到空文案 → 新增 SchedulerUtil.require_schedulable() 收敛「判定 + 抛异常 + 兜底文案」,调用点改为一行 验证:basedpyright 0 errors(原 247);ruff All checks passed;pytest 97 passed; 已部署并线上复验(四端 200、Host 收窄、伪造令牌被拒、验证码过期 400 保留业务文案、 容器内 require_schedulable 与 OSS 调用修正均已生效)
1 parent b388255 commit a4e8727

5 files changed

Lines changed: 136 additions & 13 deletions

File tree

‎AUDIT-REPORT.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,8 @@
177177
| 后端(全局兜底修正) | SQLAlchemyError 非完整性错误由 400 改为 503(连接)/500(其它),文案不再拼 `exc_type` | 读码 + 探针实测 409/503/503/500/500 |
178178
| 前端(错误文案) | 错误提示改为「后端 msg 优先 → 业务码兜底 → 状态码兜底 → 通用」,去掉按 code 白名单取文案的耦合(含 Blob 下载分支;401 排除在解析外以不影响静默续期) | type-check exit 0;vitest 41 用例(含 500+4500 与 404+404 文案断言);成功下载分支零改动 |
179179
| 存储适配器(类型检查暴露的运行时缺陷) | ① OSS:`Client.uploader` 是**方法**,原写法 `self.client.uploader.upload_file(...)` 在绑定方法上取属性 → 运行时 AttributeError(OSS 上传必失败);② SFTP/FTP:`config.host` 可空却直接传给要求 `str` 的 `connect(hostname/host)` | ① 改为 `self.client.uploader().upload_file(...)`(已按 SDK 签名核对 part_size/parallel_num);② 补主机校验,缺主机时返回 400 业务异常;basedpyright 这三个文件由 4 error → 0;运行时探针:缺主机拦截、配主机放行 |
180+
| 类型检查校准(新增 `backend/pyrightconfig.json`) | 仓库此前无 pyright 配置,basedpyright 走自身默认:`app/` 下 247 errors / 3799 warnings,143 条是 `reportMissingTypeArgument` 泛型注解类噪音,掩盖真缺陷 | `typeCheckingMode: basic` + 显式保留能抓 bug 的规则为 error + tests/ 用 `executionEnvironments` 单独放宽 → **0 errors**;做了红绿验证:合成 `str | None` 传参、`MethodType` 属性误用、Optional 成员访问三类仍全部报 error |
181+
| 类型检查暴露的两处真缺陷(我们上一轮引入) | ① `template_safety._SQLALCHEMY_TYPE_WHITELIST` 对全大写常量二次赋值;② 同文件 `normalize_column_type` 假定 `_TYPE_BASE_RE` 必命中(用 `# type: ignore` 盖住,None 时 `.group()` 会崩);③ `node/service.py` 两处 `CustomException(msg=reason)` 中 `reason` 为 `str | None` → 客户端可能收到空文案 | ① 一次成型;② 改 None 安全并去掉 type-ignore;③ 新增 `SchedulerUtil.require_schedulable()` 收敛「判定 + 抛异常 + 兜底文案」,两个调用点改为一行 |
180182

181183
### 9.2 仍未处理(需后续立项或产品决策)
182184

‎backend/app/core/ap_scheduler.py‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131

3232
from app.config.setting import settings
3333
from app.core.database import engine
34+
from app.core.exceptions import CustomException
3435
from app.core.logger import logger
3536

3637
# 任务状态常量(与 task_job.status 注释保持一致:0待执行 1执行中 2成功 3失败)
@@ -592,6 +593,24 @@ def describe_schedulability(cls, code_block: str | None) -> tuple[bool, str | No
592593
)
593594
return True, None
594595

596+
@classmethod
597+
def require_schedulable(cls, code_block: str | None) -> None:
598+
"""不可调度时抛出业务异常;可调度时直接返回。
599+
600+
把「取判定结果 + 抛异常」收敛到一处:``describe_schedulability`` 的 reason 是
601+
``str | None``(可调度时为 None),调用点直接 ``CustomException(msg=reason)`` 会
602+
传入 None 让客户端收到空文案;这里统一兜底,也避免每个调用点各写一份兜底文案。
603+
604+
参数:
605+
- code_block (str | None): 节点的 func 字段。
606+
607+
异常:
608+
- CustomException: 当前不可调度时抛出(含可操作原因)。
609+
"""
610+
schedulable, reason = cls.describe_schedulability(code_block)
611+
if not schedulable:
612+
raise CustomException(msg=reason or "当前任务不可调度,请检查任务配置")
613+
595614
@classmethod
596615
def job_code_block(cls, job: Job | None) -> str | None:
597616
"""从调度器 job 的 args 中取回 func(约定见 _add_job_with_trigger: args=[job_id, code_block, *args])。

‎backend/app/modules/generator/gencode/template_safety.py‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import ast
2929
import keyword
3030
import re
31+
from collections.abc import Iterable
3132

3233
from app.common.constant import GenConstant
3334

@@ -305,11 +306,10 @@ def assert_python_syntax(content: str, *, filename: str = "<generated>") -> None
305306
# 基名里需要剔除的修饰词(它们不属于类型名,参与查表会导致 miss)
306307
_BASE_MODIFIER_RE = re.compile(r"\s+(unsigned|zerofill)$", re.IGNORECASE)
307308

308-
# 映射常量值的白名单(类型名只能取自这里,杜绝自由文本进产物)
309-
_SQLALCHEMY_TYPE_WHITELIST: frozenset[str] = frozenset(GenConstant.DB_TO_SQLALCHEMY.values())
310-
311-
# Boolean 由 MySQL tinyint(1) 规则显式产出(常量表中未必有该值),显式纳入白名单
312-
_SQLALCHEMY_TYPE_WHITELIST = _SQLALCHEMY_TYPE_WHITELIST | {"Boolean"}
309+
# 映射常量值的白名单(类型名只能取自这里,杜绝自由文本进产物)。
310+
# Boolean 由 MySQL tinyint(1) 规则显式产出(常量表中未必有该值),此处一次成型,
311+
# 避免对全大写常量二次赋值(类型检查会报 reportConstantRedefinition,也是不必要的可变性)。
312+
_SQLALCHEMY_TYPE_WHITELIST: frozenset[str] = frozenset(GenConstant.DB_TO_SQLALCHEMY.values()) | {"Boolean"}
313313

314314

315315
def _lookup_sqlalchemy_type(base: str) -> str:
@@ -338,7 +338,11 @@ def normalize_column_type(column_type: object) -> str:
338338
"""
339339
text = "" if column_type is None else str(column_type).strip()
340340
if _ENUM_SET_RE.match(text):
341-
return _TYPE_BASE_RE.match(text).group(0).strip() # type: ignore[union-attr]
341+
# 不假定 _TYPE_BASE_RE 一定命中:None 安全处理,命中不了就原样返回,
342+
# 交给 is_valid_column_type 的形状白名单继续拦截(不在这里静默放行)。
343+
base_match = _TYPE_BASE_RE.match(text)
344+
if base_match is not None:
345+
return base_match.group(0).strip()
342346
return text
343347

344348

@@ -589,7 +593,7 @@ def assert_text_structure(
589593
reference: str,
590594
*,
591595
sentinel: str,
592-
dynamic_values: object = (),
596+
dynamic_values: Iterable[str] = (),
593597
filename: str = "<generated>",
594598
) -> None:
595599
"""``.vue`` / ``.ts`` / ``.toml`` 产物的粗粒度结构护栏。

‎backend/app/modules/task/cronjob/node/service.py‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -327,9 +327,7 @@ def _add_job_with_trigger(
327327
raise ValueError("任务代码块不能为空")
328328
# 可调度性判定(单一事实来源):builtin: 引用走白名单、不经 exec,因此**开关关闭时也允许注册**;
329329
# 只有"原始代码块"才会被 SCHEDULER_ALLOW_CODE_EXEC=False 拦住,并在提示里给出 builtin 替代写法。
330-
schedulable, reason = SchedulerUtil.describe_schedulability(code_block)
331-
if not schedulable:
332-
raise CustomException(msg=reason)
330+
SchedulerUtil.require_schedulable(code_block)
333331

334332
jobstore = job_info.jobstore or "default"
335333
executor = job_info.executor or "threadpool"
@@ -416,9 +414,7 @@ def run_node_once(node: NodeModel) -> str:
416414
if not node.func or not node.func.strip():
417415
raise ValueError("任务代码块不能为空")
418416
# 手动执行同样按可调度性判定:builtin 引用可直接执行;原始代码块在开关关闭时给出替代写法
419-
schedulable, reason = SchedulerUtil.describe_schedulability(node.func)
420-
if not schedulable:
421-
raise CustomException(msg=reason)
417+
SchedulerUtil.require_schedulable(node.func)
422418
temp_job_id = f"{node.id}{_MANUAL_JOB_PREFIX}{datetime.now():%Y%m%d%H%M%S}"
423419
trigger = DateTrigger(run_date=datetime.now() + timedelta(seconds=0.1), timezone="Asia/Shanghai")
424420
_add_job_with_trigger(node, trigger, job_id_override=temp_job_id, name_suffix=" - 手动执行")

‎backend/pyrightconfig.json‎

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
{
2+
// 校准配置:让编辑器只报「真问题」,而不是基于默认推荐规则产生大量噪音。
3+
// 背景:仓库此前没有 pyright/basedpyright 配置,basedpyright 使用自身默认
4+
// (相当于 recommended),在 app/ 下产生 247 errors / 3799 warnings,其中
5+
// 143 条是 reportMissingTypeArgument(未参数化泛型,如 Task / Result / ColumnElement),
6+
// 属「注解完整度」而非缺陷,会掩盖 reportArgumentType / reportAttributeAccessIssue
7+
// 这类能抓到真 bug 的规则(例如 OSS 的 Client.uploader 方法误用)。
8+
"typeCheckingMode": "basic",
9+
"pythonVersion": "3.12",
10+
"venvPath": ".",
11+
"venv": ".venv",
12+
"include": ["app", "main.py", "tests"],
13+
"exclude": ["**/__pycache__", ".venv", ".mypy_cache", ".ruff_cache", "alembic/versions"],
14+
15+
// tests/ 单独放宽:测试普遍用 SimpleNamespace 假对象、从 fixture 取 Optional 后直接使用,
16+
// 这些由 pytest 运行期验证;对 app/ 的严格规则保持不变。
17+
"executionEnvironments": [
18+
{
19+
"root": "tests",
20+
"extraPaths": ["."],
21+
"reportArgumentType": "none",
22+
"reportCallIssue": "none",
23+
"reportOptionalSubscript": "none",
24+
"reportOptionalMemberAccess": "none",
25+
"reportOperatorIssue": "none",
26+
"reportUnusedCallResult": "none"
27+
},
28+
{
29+
"root": "."
30+
}
31+
],
32+
33+
// ── 保留为 error(能抓真缺陷,不允许降级)──
34+
"reportAttributeAccessIssue": "error",
35+
"reportArgumentType": "error",
36+
"reportAssignmentType": "error",
37+
"reportCallIssue": "error",
38+
"reportReturnType": "error",
39+
"reportGeneralTypeIssues": "error",
40+
"reportOptionalMemberAccess": "error",
41+
"reportOptionalCall": "error",
42+
"reportOptionalSubscript": "error",
43+
"reportOptionalIterable": "error",
44+
"reportOptionalOperand": "error",
45+
"reportIndexIssue": "error",
46+
"reportOperatorIssue": "error",
47+
"reportInvalidTypeArguments": "error",
48+
"reportConstantRedefinition": "error",
49+
"reportRedeclaration": "error",
50+
"reportUndefinedVariable": "error",
51+
"reportSelfClsParameterName": "error",
52+
"reportInvalidStringEscapeSequence": "error",
53+
"reportUnhashable": "error",
54+
"reportInvalidCast": "error",
55+
"reportAssertAlwaysTrue": "error",
56+
"reportIncompatibleMethodOverride": "error",
57+
"reportOverlappingOverload": "error",
58+
"reportNoOverloadImplementation": "error",
59+
"reportAbstractUsage": "error",
60+
"reportMissingImports": "error",
61+
"reportMissingModuleSource": "warning",
62+
63+
// SQLAlchemy 声明式模型的已知误报族:基类用 @declared_attr.directive 生成
64+
// __tablename__/__table_args__(pyright 视作描述符/类变量),而各模型普遍用普通
65+
// 类属性覆写(__tablename__: str = "sys_user"),pyright 判定为「实例变量覆写类变量」。
66+
// 试过在基类 TYPE_CHECKING 里补 ClassVar 声明的正规解法:反而新增 reportRedeclaration
67+
// 且错误数上升(62 → 90),故按社区通行做法关闭该规则。真实的方法覆写不兼容由
68+
// reportIncompatibleMethodOverride(保持 error)兜底。
69+
"reportIncompatibleVariableOverride": "none",
70+
"reportUnboundVariable": "error",
71+
72+
// ── 降级为 none:注解完整度/风格类,属增量清理而非缺陷 ──
73+
"reportMissingTypeArgument": "none",
74+
"reportUnknownMemberType": "none",
75+
"reportUnknownArgumentType": "none",
76+
"reportUnknownVariableType": "none",
77+
"reportUnknownParameterType": "none",
78+
"reportUnknownLambdaType": "none",
79+
"reportMissingParameterType": "none",
80+
"reportUnannotatedClassAttribute": "none",
81+
"reportImplicitOverride": "none",
82+
"reportMissingTypeStubs": "none",
83+
"reportAny": "none",
84+
"reportExplicitAny": "none",
85+
"reportUnusedCallResult": "none",
86+
"reportUnusedImport": "none",
87+
"reportPrivateImportUsage": "none",
88+
"reportPrivateLocalImportUsage": "none",
89+
"reportUnnecessaryComparison": "none",
90+
"reportUnnecessaryIsInstance": "none",
91+
"reportUnnecessaryCast": "none",
92+
"reportUnusedParameter": "none",
93+
"reportUnreachable": "none",
94+
"reportImplicitStringConcatenation": "none",
95+
"reportUnsafeMultipleInheritance": "none",
96+
"reportDeprecated": "none",
97+
"reportUnusedClass": "none",
98+
"reportUnusedFunction": "none",
99+
"reportUnusedVariable": "none",
100+
"reportWildcardImportFromLibrary": "none",
101+
"reportUnsupportedDunderAll": "none"
102+
}

0 commit comments

Comments
 (0)