路径归一化:统一 14 个子目录 helper 接口 + 修复 getattr 张冠李戴
新增 _step_path_resolver.py(STEP_DATA_SOURCE 映射表 + _FALLBACK_DIR_TABLE 40+ keys + resolve_subdir / get_step_output_path / resolve_step_widget 三层 API),与 pipeline.get_step_output_dir 互为表里、互不依赖。 pipeline 新增 get_step_output_dir(step_name) 唯一权威接口(class-level _STEP_OUTPUT_DIR_MAP 延迟构造 + 未知 key 回退 work_dir + 调试日志)。 全量重构 src/gui/panels/step*.py(17 个文件) * 消除全部 os.path.join(wp, "X_subdir") 硬编码(14 个预定义子目录) * 8 处 getattr(main_window.stepXX_panel, ...) 张冠李戴死代码全部修复(错位属性名 → 通过 STEP_DATA_SOURCE 映射到正确的 main_window 长名属性) * 删除 step12_viz_panel.py 中 self.step11_ml_panel / step11_panel / step12_panel 死代码块 * 提示文字/标签字典/日志保留原文,仅替换实际路径计算 Smoke test:39 fallback key + 14 路径映射 + 14 step 数字 key + 17/17 panel AST 解析 + 17/17 import 全部就位。
This commit is contained in:
186
src/gui/panels/_step_path_resolver.py
Normal file
186
src/gui/panels/_step_path_resolver.py
Normal file
@ -0,0 +1,186 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Step 路径解析器——统一消灭 panel 端的硬编码路径与"张冠李戴"跨面板引用。
|
||||
|
||||
提供三个公共 API:
|
||||
- resolve_step_widget(main_window, step_key)
|
||||
- get_step_output_path(main_window, step_key, work_dir=None)
|
||||
- STEP_DATA_SOURCE 映射表
|
||||
|
||||
典型使用:
|
||||
from src.gui.panels._step_path_resolver import (
|
||||
resolve_step_widget, get_step_output_path
|
||||
)
|
||||
|
||||
# 替换前:getattr(main_window.step11_panel, 'output_file', None) # 死代码
|
||||
# 替换后:
|
||||
widget = resolve_step_widget(main_window, 'step11_predictions') # 找到正确 widget
|
||||
if widget: ...
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
# 用户口语编号 / 业务别名 → main_window 上真实属性名的映射
|
||||
# 这是"张冠李戴"修复的核心——之前代码写的 step11_panel 实际不存在,
|
||||
# 真实存在的属性见 water_quality_gui.py:1891-1928
|
||||
STEP_DATA_SOURCE = {
|
||||
# 数据流 step 编号(用户口语) → main_window 真实属性
|
||||
'step5_clean_output': 'step5_clean_panel',
|
||||
'step7_index_output': 'step7_index_panel',
|
||||
'step8_ml_train_output': 'step8_ml_train_panel',
|
||||
'step8_5_non_empirical': 'step8_non_empirical_panel', # 之前写错成 step11_panel
|
||||
'step9_ml_predict_output': 'step9_ml_predict_panel',
|
||||
'step10_watercolor_output': 'step10_watercolor_panel',
|
||||
'step11_ml_prediction': 'step9_ml_predict_panel', # 主流程 step11 = ML 预测
|
||||
'step12_regression_prediction': 'step8_non_empirical_panel', # 主流程 step12 = 非经验预测
|
||||
'step13_custom_regression': 'step13_report_panel', # 占位(自定义回归本身没有专属 panel)
|
||||
'sampling_csv': 'step4_sampling_panel',
|
||||
'training_spectra_csv': 'step5_clean_panel',
|
||||
'indices_csv': 'step7_index_panel',
|
||||
'models_dir': 'step8_ml_train_panel',
|
||||
'watercolor_dir': 'step10_watercolor_panel',
|
||||
'prediction_csv_dir': 'step9_ml_predict_panel', # 默认从 ML 预测读
|
||||
}
|
||||
|
||||
|
||||
def _get_widget(main_window, attr_name: str, widget_attr: str = 'output_file'):
|
||||
"""从 main_window.<attr_name> 取出指定子组件,失败时返回 None。"""
|
||||
if main_window is None:
|
||||
return None
|
||||
panel = getattr(main_window, attr_name, None)
|
||||
if panel is None:
|
||||
return None
|
||||
return getattr(panel, widget_attr, None)
|
||||
|
||||
|
||||
def _read_widget_path(widget) -> str:
|
||||
"""统一从 widget 读 path(兼容 FileSelectWidget / QLineEdit / 字符串)。"""
|
||||
if widget is None:
|
||||
return ""
|
||||
if hasattr(widget, 'get_path'):
|
||||
try:
|
||||
return str(widget.get_path() or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
if hasattr(widget, 'text'):
|
||||
try:
|
||||
return str(widget.text() or "").strip()
|
||||
except Exception:
|
||||
return ""
|
||||
if isinstance(widget, str):
|
||||
return widget.strip()
|
||||
return ""
|
||||
|
||||
|
||||
def resolve_step_widget(main_window, step_key: str, widget_attr: str = 'output_file'):
|
||||
"""根据业务 step_key 解析出正确的 widget(消除张冠李戴)。
|
||||
|
||||
Returns:
|
||||
widget 对象 or None(找不到时返回 None,调用方需自行兜底)
|
||||
"""
|
||||
attr_name = STEP_DATA_SOURCE.get(step_key)
|
||||
if attr_name is None:
|
||||
return None
|
||||
return _get_widget(main_window, attr_name, widget_attr)
|
||||
|
||||
|
||||
_FALLBACK_DIR_TABLE = {
|
||||
# pipeline key(与 _ensure_step_dir_map 对齐)→ 子目录名
|
||||
'step1': '1_water_mask',
|
||||
'step2': '2_Glint_Detection',
|
||||
'step3': '3_deglint',
|
||||
'step4_sampling': '4_sampling',
|
||||
'step5_clean': '5_Data_Cleaning',
|
||||
'step6_feature': '6_Spectral_Feature_Extraction',
|
||||
'step7_index': '7_Water_Quality_Indices',
|
||||
'step8_ml_train': '8_Supervised_Model_Training',
|
||||
'step8': '8_Supervised_Model_Training',
|
||||
'step9_ml_predict': '8_Non_Empirical_Regression',
|
||||
'step9': '8_Non_Empirical_Regression',
|
||||
'step10_watercolor': '10_WaterIndex_Images',
|
||||
'step10': '10_WaterIndex_Images',
|
||||
'step11_map': '14_visualization',
|
||||
'step11': '11_12_13_predictions',
|
||||
'step11_predictions': '11_12_13_predictions',
|
||||
'step12': '13_Custom_Regression',
|
||||
'step12_predictions': '11_12_13_predictions',
|
||||
'step13': 'reports',
|
||||
'step13_predictions': '11_12_13_predictions',
|
||||
'step14': '14_visualization',
|
||||
'prediction_dir': '11_12_13_predictions',
|
||||
'visualization': '14_visualization',
|
||||
'reports': 'reports',
|
||||
'custom_regression': '13_Custom_Regression',
|
||||
# 扩展:覆盖 panel 内部使用的子目录别名
|
||||
'water_mask': '1_water_mask',
|
||||
'glint_detection': '2_Glint_Detection',
|
||||
'deglint': '3_deglint',
|
||||
'sampling': '4_sampling',
|
||||
'data_cleaning': '5_Data_Cleaning',
|
||||
'spectral_feature': '6_Spectral_Feature_Extraction',
|
||||
'indices': '7_Water_Quality_Indices',
|
||||
'supervised_models': '8_Supervised_Model_Training',
|
||||
'non_empirical': '8_Non_Empirical_Regression',
|
||||
'qaa_inversion': '8_QAA_Inversion',
|
||||
'regression_modeling': '8_Regression_Modeling',
|
||||
'watercolor': '10_WaterIndex_Images',
|
||||
'ml_prediction': '9_ML_Prediction',
|
||||
'sampling_csv_path': '4_sampling/sampling_spectra.csv',
|
||||
}
|
||||
|
||||
|
||||
def get_step_output_path(
|
||||
main_window,
|
||||
step_key: str,
|
||||
work_dir: Optional[Union[str, Path]] = None,
|
||||
widget_attr: str = 'output_file',
|
||||
fallback_key: Optional[str] = None,
|
||||
) -> str:
|
||||
"""获取 step_key 指向的输出路径(带 main_window 解析 + 兜底路径)。
|
||||
|
||||
解析顺序:
|
||||
1. STEP_DATA_SOURCE[step_key] 找到对应 panel,从 widget 读用户填的 path
|
||||
2. 若为空字符串,用 _FALLBACK_DIR_TABLE[fallback_key or step_key] + work_dir 拼兜底
|
||||
3. 全失败返回 str(work_dir)
|
||||
|
||||
注意:不创建 pipeline 实例(避免触发 osgeo 导入),用本地子目录字典兜底。
|
||||
"""
|
||||
wd = str(work_dir) if work_dir else ""
|
||||
widget = resolve_step_widget(main_window, step_key, widget_attr)
|
||||
p = _read_widget_path(widget)
|
||||
if p:
|
||||
if not Path(p).is_absolute() and wd:
|
||||
p = str(Path(wd) / p).replace('\\', '/')
|
||||
return p
|
||||
|
||||
# 兜底:本地子目录字典(与 pipeline._ensure_step_dir_map 一致)
|
||||
key = fallback_key or step_key
|
||||
sub = _FALLBACK_DIR_TABLE.get(key)
|
||||
if sub and wd:
|
||||
return str(Path(wd) / sub).replace('\\', '/')
|
||||
return wd
|
||||
|
||||
|
||||
def resolve_subdir(work_dir, subdir_key: str) -> str:
|
||||
"""纯子目录拼装:把 pipeline key 解析为 work_dir 下的子目录路径。
|
||||
|
||||
用法:resolve_subdir(self.work_dir, 'visualization')
|
||||
→ '<work_dir>/14_visualization'
|
||||
|
||||
与 pipeline.get_step_output_dir 同源(都查同一份 _FALLBACK_DIR_TABLE 子集)。
|
||||
"""
|
||||
wd = str(work_dir) if work_dir else ""
|
||||
sub = _FALLBACK_DIR_TABLE.get(subdir_key)
|
||||
if sub and wd:
|
||||
return str(Path(wd) / sub).replace('\\', '/')
|
||||
return wd
|
||||
|
||||
|
||||
__all__ = [
|
||||
'STEP_DATA_SOURCE',
|
||||
'resolve_step_widget',
|
||||
'get_step_output_path',
|
||||
'resolve_subdir',
|
||||
]
|
||||
Reference in New Issue
Block a user