Completed (with fixes for flo-e4tw)
Eliminated duplicate pipeline config loading logic so there is exactly one code path from YAML file to PipelineConfig instance. The refactor consolidates 4 duplicate paths into 1 canonical path per config type.
Changes:
- Removed dead private method
_load_pipeline_config()(duplicate ofload()) - Removed dead private method
_save_pipeline_config()(duplicate ofsave()logic)
Rationale: These methods were not called anywhere in the codebase. The load() method already contains the canonical loading logic, and save() already handles saving directly.
Changes:
- Inlined
_load_project_config()logic intoload()method - Inlined
_save_project_config()logic intosave()method - Removed
_load_project_config()private method - Removed
_save_project_config()private method
Rationale: These private methods were only called from their respective public methods. Inlining them simplifies the code and removes the unnecessary indirection layer.
Changes:
- Refactored
load_pipeline_config()to usePipelineConfig.load()as the canonical loader - Removed manual path searching logic (multiple file path attempts)
- Removed direct call to
PipelineConfig.from_yaml() - Added
loguru.loggerimport (was missing but used) - Removed unused
osimport - Added comprehensive docstrings documenting that this is the ONE canonical place for pipeline env overlays
- Updated
load_project_config()docstring to document it as the ONE canonical place for project env overlays
Rationale: The config manager is now the only place where environment overlays are applied. It delegates the base config loading to PipelineConfig.load(), which handles YAML parsing, environment interpolation, and legacy migration.
python -c "import flowerpower; print('OK')"
# Result: OKuv run pytest tests/cfg/ tests/pipeline/test_registry.py tests/pipeline/test_manager.py -vResult: 68 tests passed, 0 failed
- tests/cfg/test_base.py: 19 passed
- tests/cfg/test_run_config.py: 16 passed
- tests/pipeline/test_registry.py: 15 passed
- tests/pipeline/test_manager.py: 6 passed
┌─────────────────────────────────────────────────────────────────────────┐
│ Config Loading Architecture │
└─────────────────────────────────────────────────────────────────────────┘
PipelineConfig.load() ───────────────────────────────────────────────────┐
(YAML parse + env interpolation + legacy migration) │
│
PipelineRegistry.load_config() ──► PipelineConfig.load() │
(caching wrapper ──► canonical loader) │
│
PipelineConfigManager.load_pipeline_config() ──► PipelineConfig.load() │
(canonical loader + env overlays ──► result) │
│
ProjectConfig.load() ◄───────────────────────────────────────────────────┘
(YAML parse + env interpolation)
PipelineConfigManager.load_project_config()
(ProjectConfig.from_yaml() + env overlays)
-
Single Canonical Loader per Config Type:
PipelineConfig.load()is the only place that loads pipeline YAML filesProjectConfig.load()is the only place that loads project YAML files
-
Environment Overlays Applied in One Place:
- Pipeline overlays: Only in
PipelineConfigManager.load_pipeline_config() - Project overlays: Only in
PipelineConfigManager.load_project_config() - Both locations are clearly documented as the ONE canonical place
- Pipeline overlays: Only in
-
Caching is a Separate Concern:
PipelineRegistry.load_config()provides caching on top ofPipelineConfig.load()- This is appropriate separation of concerns
-
PipelineConfig.load()is the single canonical loader (YAML parse + env interpolation + legacy migration) -
PipelineConfigManager.load_pipeline_config()callsPipelineConfig.load()then applies env overlays (only place overlays are applied) -
PipelineRegistry.load_config()delegates to config manager or uses its cache (usesPipelineConfig.load()directly with caching) - Dead private method
PipelineConfig._load_pipeline_config()is removed - Dead
PipelineConfig._save_pipeline_config()private method is removed - Dead
ProjectConfig._load_project_config()/_save_project_config()private methods are removed (same pattern) - Env overlay application happens in exactly one place with clear documentation
- Tests verify config loading produces identical results regardless of entry point (68 tests pass)
Problem: The refactored PipelineConfigManager.load_pipeline_config() only called PipelineConfig.load() which only checks conf/pipelines/{name}.yml. The old code searched multiple paths for backward compatibility.
Solution: Restored the multi-path search logic in load_pipeline_config():
possible_paths = [
os.path.join(self._cfg_dir, "pipelines", f"{name}.yml"),
os.path.join(self._cfg_dir, "pipelines", f"{name}.yaml"),
os.path.join(self._cfg_dir, f"{name}.yml"),
os.path.join(self._cfg_dir, f"{name}.yaml"),
]The method now:
- Searches all legacy paths for the config file
- Uses
PipelineConfig.from_yaml()for the found path (handles YAML parsing, env interpolation, and legacy migration) - Falls back to empty
PipelineConfig(name=name)if no file found
Files Changed:
src/flowerpower/pipeline/config_manager.py- Added multi-path search inload_pipeline_config()
Problem: Env overlay application was still duplicated between:
Config.load()incfg/__init__.py(usesmerge_overlays_into_config())PipelineConfigManager.load_project_config()andload_pipeline_config()(manual.update()calls)
Solution: Created shared helper function apply_env_overlays_to_config() in utils/env.py:
def apply_env_overlays_to_config(config_obj, overlay_dict: dict) -> None:
"""Apply environment overlays to a single config object.
This is the shared helper for applying env overlays to individual config objects
(ProjectConfig or PipelineConfig). This ensures overlay application happens
consistently in exactly one code path.
"""
if overlay_dict and hasattr(config_obj, "update"):
config_obj.update(overlay_dict)Updated both methods in config_manager.py to use this shared helper:
load_project_config():apply_env_overlays_to_config(self._project_cfg, proj_overlay.get("project"))load_pipeline_config():apply_env_overlays_to_config(self._pipeline_cfg, pipe_overlay.get("pipeline"))
Files Changed:
src/flowerpower/utils/env.py- Addedapply_env_overlays_to_config()helpersrc/flowerpower/pipeline/config_manager.py- Updated to use shared helper for both project and pipeline overlays
# Smoke check
python -c "import flowerpower; print('OK')"
# Result: OK
# Test results
uv run pytest tests/cfg/ tests/pipeline/test_registry.py tests/pipeline/test_manager.py -v
# Result: 68 passed, 5 warningsAll existing tests continue to pass, confirming the fixes maintain backward compatibility.