16 KiB
name, description, source, extracted_at
| name | description | source | extracted_at |
|---|---|---|---|
| PipelineRunner Facade 防御性 kwargs 兜底 | WQ_GUI 14 个 stepX_... Facade 方法必须以 **kwargs 收尾——配合 PipelineRunner 调度模式杜绝 "unexpected keyword argument" TypeError | auto-skill | 2026-06-04T00:54:50.036Z |
PipelineRunner Facade 防御性 kwargs 兜底
适用场景
在 WQ_GUI 中,任何被 PipelineRunner 调用的 14 个 stepX_... Facade 方法(位于 src/core/water_quality_inversion_pipeline_GUI.py),其形参表末尾必须带 **kwargs。触发信号:
- 用户报错
TypeError: stepX_xxx() got an unexpected keyword argument 'yyy' - 改 PIPELINE_STEPS 的
requires列表 - 新增 / 重命名一个 step 方法
- 重构 PipelineRunner 的
_invoke注入逻辑
核心原则
Facade 的形参表 = 显式声明的形参 + , **kwargs。kwargs 必须严格位于形参表最后(Python 语法硬要求)。
# ✅ 正确
def step3_remove_glint(self, img_path: str,
method: str = "subtract_nir",
# ... 30+ 业务形参 ...
skip_dependency_check: bool = False,
**kwargs) -> str:
...
# ❌ 错误:**kwargs 不能放中间或前面
def step3_remove_glint(self, img_path, **kwargs, skip_dependency_check): # SyntaxError
为什么需要这层防御
PipelineRunner._invoke(src/core/pipeline/runner.py)会向方法注入两类参数:
| 层 | 来源 | 形参 key 怎么定 |
|---|---|---|
| L2 | ctx 字段(按 spec.requires 列表) |
_default_param_name(ctx_key) 默认去 _path 后缀 |
| L3 | ctx.user_config[step_id](14 panel dict 整体) |
dict 的 key 原样注入 |
L2 触发 TypeError 的真实场景(2026-06-04 真实发生):
PIPELINE_STEPS.step3.requires = ["img_path", "water_mask_path", "glint_mask_path"]- Runner 注入
kwargs["glint_mask_path"] = ctx.glint_mask_path step3_remove_glint形参表没有glint_mask_path(虽然业务上耀斑掩膜是在子调用GlintRemovalStep.run内部用的,Facade 本身不接)- → TypeError: step3_remove_glint() got an unexpected keyword argument 'glint_mask_path'
L3 触发 TypeError 的场景:
- user_config 14 panel dict 里残留了旧字段名 / 跨 step 串味的字段
- 任何
user_config[step_id][k]中的k都会被注入
**kwargs 一次性解决两类问题。
14 个 Facade 方法清单(截至 2026-06-04 已全部带 **kwargs)
| step | method | 形参表闭合示例 |
|---|---|---|
| 1 | step1_generate_water_mask |
output_path: Optional[str] = None, **kwargs) -> str: |
| 2 | step2_find_glint_area |
skip_dependency_check: bool = False, **kwargs) -> str: |
| 3 | step3_remove_glint |
skip_dependency_check: bool = False, **kwargs) -> str: |
| 4 | step4_process_csv |
skip_dependency_check: bool = False, **kwargs) -> str: |
| 5 | step5_extract_training_spectra |
skip_dependency_check: bool = False, **kwargs) -> str: |
| 5.5 | step5_5_calculate_water_quality_indices |
skip_dependency_check: bool = False, **kwargs) -> str: |
| 6 | step6_train_models |
skip_dependency_check: bool = False, **kwargs) -> str: |
| 6.5 | step6_5_non_empirical_modeling |
skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]: |
| 6.75 | step6_75_custom_regression |
skip_dependency_check: bool = False, **kwargs) -> str: |
| 7 | step7_generate_sampling_points |
skip_dependency_check: bool = False, **kwargs) -> str: |
| 8 | step8_predict_water_quality |
skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]: |
| 8.5 | step8_5_predict_with_non_empirical_models |
skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]: |
| 8.75 | step8_75_predict_with_custom_regression |
skip_dependency_check: bool = False, **kwargs) -> Dict[str, str]: |
| 9 | step9_generate_distribution_map |
skip_dependency_check: bool = False, **kwargs) -> str: |
标准操作
1. 编辑(最小外科手术式)
每个方法的最后形参是 skip_dependency_check: bool = False,把这一行改成:
skip_dependency_check: bool = False, **kwargs) -> str:
注意缩进必须与原行一致(13 空格 / 35 空格 / 47 空格 / 48 空格不等,按方法原始缩进)。用 edit 工具的 old_string 必须含 docstring 第一行("""步骤X: ...""")作唯一标识。
2. 验证
写一个临时校验脚本(项目根目录运行后删掉):
import ast, re
target = r'D:\111\office\ZHLduijie\1.WQ\WQ_GUI\src\core\water_quality_inversion_pipeline_GUI.py'
src = open(target, encoding='utf-8-sig').read()
ast.parse(src) # AST 语法
expected = [
'step1_generate_water_mask', 'step2_find_glint_area', 'step3_remove_glint',
'step4_process_csv', 'step5_extract_training_spectra',
'step5_5_calculate_water_quality_indices', 'step6_train_models',
'step7_generate_sampling_points', 'step8_predict_water_quality',
'step9_generate_distribution_map', 'step6_5_non_empirical_modeling',
'step6_75_custom_regression', 'step8_5_predict_with_non_empirical_models',
'step8_75_predict_with_custom_regression',
]
pat = re.compile(r'def\s+(\w+)\s*\((?:[^()]|\([^()]*\))*\*\*kwargs\)\s*->\s*[^:]+:', re.DOTALL)
found = set(pat.findall(src))
print('Missing:', [m for m in expected if m not in found])
print('Extra :', [m for m in found if m not in expected])
期望输出:两个列表都为空。
3. Windows 执行
cd /d D:\111\office\ZHLduijie\1.WQ\WQ_GUI
py _check.py
del _check.py
必加
utf-8-sig:water_quality_inversion_pipeline_GUI.py头部可能含 BOM(code_replacement_state_auditskill 里有同样提示)。
⚠️ 这层防御不能解决什么
**kwargs 兜底 ≠ 形参名错位修复。如果 Runner 注入 kwargs["training_csv_path"] 而方法形参是 csv_path:
- ✅ 不会报 TypeError(
training_csv_path被**kwargs收走) - ❌ 但
csv_path仍是 None(方法体内的if csv_path is not None: ... else: ...走 fallback 分支,可能读self.training_csv_path哨兵)
已知形参名错位的方法(2026-06-04 已修,commit 64aa5b8):
| step | Runner 注入的 ctx key | 方法实际形参 | 实际落地的修复 |
|---|---|---|---|
| step6_5 | training_csv_path |
csv_path |
parameter_map={"training_csv_path": "csv_path"} |
| step6_75 | (已切到 indices_path) |
csv_path |
⚠️ 见下方"step6_75 路由修复"专题 |
| step8_5 | models_dir |
non_empirical_models_dir |
parameter_map={"models_dir": "non_empirical_models_dir"} |
| step8_75 | models_dir |
custom_regression_dir |
parameter_map={"models_dir": "custom_regression_dir"} |
parameter_map 是
StepSpec已有字段(runner.py:33),作用是把 ctx 字段重命名到方法形参名。优先用 parameter_map 而非改 requires——保持 ctx 字段语义清晰(声明式描述上游依赖),形参名是方法私有约定。
step6_75 路由修复(特殊案例,2026-06-04 commit 64aa5b8)
step6_75_custom_regression 不是简单的 ctx 字段名错位——方法体内的 fallback 链透露了真正的数据源是 indices_path:
# step6_75 形参:csv_path
# 方法体 fallback:
# if csv_path is not None: input_csv = csv_path
# elif self.indices_path is not None: input_csv = self.indices_path # ★ 真相
加 parameter_map 把 training_csv_path → csv_path 看似能跑通,但实际用错了数据(training_csv 是 step5 输出,不是 step6_75 想要的 indices CSV)。
正确做法 = 同时改 requires + parameter_map:
StepSpec(
step_id="step6_75", method_name="step6_75_custom_regression",
requires=["indices_path"], # ★ 从 training_csv_path 切到 indices_path
produces=["models_dir"],
parameter_map={"indices_path": "csv_path"}, # ★ 同步改 key
description="自定义回归分析",
),
配合 skip_when_missing 兜底:若用户没跑 step5_5(ctx.indices_path 为 None),runner 自动 skip 整个 step6_75,不会用错位数据静默执行。
判别何时需要"路由切"vs"纯 rename":
- 看方法体 fallback 链:fallback 到
self.indices_path/self.deglint_img_path/其他 ctx 字段名 → 需要改 requires - 仅是 key 名字不同,方法体直接用形参 → 只改 parameter_map
L2 注入顺序冲突:多个 requires 字段解析到同一形参名
场景
StepSpec.requires 里有多个 ctx 字段,经过 _default_param_name / parameter_map 解析后,会落到同一个方法形参名。L2 注入是顺序敏感的(后者覆盖前者),后注入的会默默覆盖前一个的赋值。
真实案例(2026-06-04 step5 修复)
业务需求:step5 真正需要 step4 产物 processed_csv_path,但保留 raw csv_path 字段作为 user_config 覆盖入口。
❌ 错误的 parameter_map 写法(用户原方案的隐藏 bug):
StepSpec(
step_id="step5", method_name="step5_extract_training_spectra",
requires=["deglint_img_path", "processed_csv_path", "csv_path", ...], # raw csv_path 也在
parameter_map={"processed_csv_path": "csv_path"}, # ★ 只映射了一个
...
)
L2 注入顺序(runner.py:184-186):
deglint_img_path→kwargs[deglint_img_path] = ctx.deglint_img_pathprocessed_csv_path→kwargs[csv_path] = ctx.processed_csv_path← 主路径生效csv_path(无映射 → 默认)→kwargs[csv_path] = ctx.csv_path← 后注入 None 覆盖了主路径!
症状:step5 形参 csv_path 拿到的是 raw ctx.csv_path(通常是 None),方法体 fallback 到 self.processed_csv_path——但这个 fallback 也可能是 None(step4 没跑),step5 内部空跑 → "静默错误"。
**✅ 修法:parameter_map 双向映射 + 占位名落 kwargs:
parameter_map={
"processed_csv_path": "csv_path", # 主路径(注入到方法形参)
"csv_path": "_raw_csv_ignored", # 占位(落到 step5 形参列表末尾的 **kwargs)
},
注入顺序重排后:
processed_csv_path→kwargs[csv_path] = ctx.processed_csv_path← 主路径csv_path→kwargs[_raw_csv_ignored] = ctx.csv_path← 落 **kwargs(被吞)
step5 形参 csv_path 最终拿到 ctx.processed_csv_path 的值 ✓。
验证模板(行为模拟)
写临时 _verify_l2_inject.py 复刻 runner.py:184-186 的 L2 注入循环,不要只靠 AST 静态检查——parameter_map 的 key 顺序、requires 的字段顺序都是动态的:
import sys
sys.path.insert(0, r'D:\111\office\ZHLduijie\1.WQ\WQ_GUI')
from src.core.pipeline.context import PipelineContext
from src.core.pipeline.runner import PIPELINE_STEPS
spec5 = next(s for s in PIPELINE_STEPS if s.step_id == 'step5')
# 复刻 L2 注入(与 runner.py:184-186 完全一致)
def l2_inject(spec, ctx):
kwargs = {}
for ctx_key in spec.requires:
param_name = spec.parameter_map.get(ctx_key, ctx_key) # ★ 必须原样复刻
kwargs[param_name] = ctx.get(ctx_key)
return kwargs
# 关键断言
ctx = PipelineContext(processed_csv_path='/csv/processed.csv', csv_path='/csv/raw.csv')
kw = l2_inject(spec5, ctx)
assert kw.get('csv_path') == '/csv/processed.csv', \
f"形参 csv_path 应等于 processed_csv_path, 实际 {kw.get('csv_path')!r}"
assert '_raw_csv_ignored' in kw, "占位名应被注入到 kwargs"
print(f'OK: csv_path 形参 = {kw["csv_path"]!r} (processed, 主路径正确)')
print(f'OK: _raw_csv_ignored 占位 = {kw["_raw_csv_ignored"]!r} (raw, 落 **kwargs 被吞)')
跑完删掉:py _verify_l2_inject.py & del _verify_l2_inject.py(Windows 一行模式)。
何时需要警惕这个冲突
修改 StepSpec 时检查清单(先看这一段再写 parameter_map):
- requires 里是否有多于 1 个 ctx 字段,解析后会落到同名方法形参? 典型撞车:
- 同名字段(如
processed_csv_path和csv_path都能映射到csv_path) - 同名
_default_param_name退化(如boundary_path和boundary_shp_path默认都映射到boundary_path——但要注意_default_param_name已废弃去后缀,原样返回 ctx key,所以boundary_path和boundary_shp_path默认就会撞boundary_path/boundary_shp_path不会撞,要撞就必须显式 parameter_map) - 字段名 + parameter_map 重命名撞车
- 同名字段(如
- "主路径"字段在 requires 列表靠前位置(让后续"备路径"覆盖,但这不解决冲突——只要有第二次注入就一定会覆盖)
- "备路径"字段用占位名
_xxx_ignored/_xxx_kwargs_only映射,让它落到 **kwargs - 确认方法形参表末尾有
**kwargs兜底(facade_kwargs_defenseskill 核心要求,已 14/14 落地)
反例(不要做)
- ❌ "我让
csv_path不在 requires 里就行了"——会丢失 user_config 覆盖入口(如果用户想用 raw CSV 而不是 processed) - ❌ "改 L2 注入循环,让 parameter_map 字段最后注入"——会改变 runner 通用语义,影响所有 step 的注入顺序
- ❌ "加
if param_name in kwargs: continue在 L2 注入里"——隐式"第一次优先"语义,新人读代码摸不着头脑 - ❌ "用 position in requires 做加权"——把数据语义(哪个字段优先级高)塞到列表顺序里,runner 应该保持"声明式"
与"纯 rename"的区别
| 维度 | 纯 rename(已有 skill 案例) | 多→1 冲突(本节案例) |
|---|---|---|
| 典型场景 | step6_5/6_75/8_5/8_75:1 个 requires 重命名到形参 | step5:2 个 requires 撞到同一形参 |
| parameter_map | 1 个 key→value | 2 个 key→同名 value + 占位名 |
| requires | 1 个字段 | 2 个字段(主 + 备) |
| 冲突来源 | 不会出现(单 key) | 出现(顺序敏感 + 撞名) |
| 修法 | 只加 parameter_map | 双向 parameter_map + 占位名 |
与其他防御层的关系
PipelineRunner.run() 主循环
├─ L1 runner.py:152 skip_when_missing ─── ctx.<required> 全 None → skip step
├─ L2 runner.py:182 ctx 字段注入 ─── 形参表里没声明 → TypeError ⚠️ → **kwargs 兜底
├─ L3 runner.py:188 user_config 合并 ─── user_config 有"空字符串"/None → 跳过(上一轮加的守卫)✅
└─ L4 runner.py:211 except 捕获 ─── 业务抛异常 → ctx.status="error" + raise
**kwargs 是 L2 的"消极兜底"——宁愿吞掉多余 key 也不报 TypeError。真正的"积极修复"是 parameter_map(让 ctx 字段名映射到正确形参名)。两层配合:
- 保守期间(重构初期):先
**kwargs兜住,TypeError 消失 - 稳定阶段:补 parameter_map,让方法收到正确数据
反例(不要做)
- ❌ "不写
**kwargs,靠 type hint + IDE 检查兜底"——Runner 是运行时注入,IDE 看不到 - ❌ "把
**kwargs放形参表中间"——Python 语法错误 - ❌ "改 requires 列表去掉冗余 ctx 字段"——会导致
skip_when_missing误判(以为 step 不需要该 ctx 字段),应该用parameter_map重命名而非删除 requires - ❌ "在 14 个 Facade 方法体里加
if 'glint_mask_path' in kwargs: kwargs.pop('glint_mask_path')"——脏活,且每个方法都要加,远不如**kwargs一行优雅
案例来源
- 2026-06-04 WQ_GUI PipelineRunner 迁移第二步
- 触发:
step3_remove_glint() got an unexpected keyword argument 'glint_mask_path' - 根因:
PIPELINE_STEPS.step3.requires写了glint_mask_path,但GlintRemovalStep内部使用,Facade 自身不接这个形参 - 落地:14 个 Facade 全部加
, **kwargs,0 个 TypeError - 验证:临时
_check.py14/14 命中 + AST 解析通过 - 续:4 个 parameter_map 全部落地(commit
64aa5b8),含 step6_75 路由切到 indices_path;L3 非空过滤同步加入runner._invoke:188 - 2026-06-04 step5 严格依赖修复:发现 L2 注入顺序冲突(requires 多个字段解析到同一形参名),引入"双向 parameter_map + 占位名落 **kwargs"模式;step5 形参
csv_path真正接到processed_csv_path(step4 产物),rawcsv_path保留为 user_config 覆盖入口,落占位名_raw_csv_ignored后被**kwargs吞。skip_when_missing 块同步加_notify通知,拒绝静默跳过(15 条 _notify 全带具体 missing 字段列表证据)。