Recursively collect all submodule/module YAML paths referenced by a workflow. This currently supports: - Formal `submodules` declarations (via normalize_submodule_declarations) - Workflow modules referenced via `module: "modules/xxx.yaml"` in the YAML
(root_plan: Path)
| 152 | |
| 153 | |
| 154 | def _collect_submodule_paths(root_plan: Path) -> Set[Path]: |
| 155 | """Recursively collect all submodule/module YAML paths referenced by a workflow. |
| 156 | |
| 157 | This currently supports: |
| 158 | - Formal `submodules` declarations (via normalize_submodule_declarations) |
| 159 | - Workflow modules referenced via `module: "modules/xxx.yaml"` in the YAML |
| 160 | """ |
| 161 | parser = YAMLTaskParser() |
| 162 | visited: Set[Path] = set() |
| 163 | stack = [root_plan] |
| 164 | root_base = root_plan.parent |
| 165 | |
| 166 | while stack: |
| 167 | current = stack.pop() |
| 168 | try: |
| 169 | yaml_data = parser.load_task(str(current)) |
| 170 | except Exception: |
| 171 | # If we can't parse a submodule, skip it but continue with others |
| 172 | continue |
| 173 | # 1) Collect submodules declared via the `submodules` field |
| 174 | try: |
| 175 | submodules = normalize_submodule_declarations(yaml_data, str(current)) |
| 176 | except Exception: |
| 177 | # If submodule declarations are invalid, skip but continue with other mechanisms |
| 178 | submodules = [] |
| 179 | |
| 180 | for entry in submodules: |
| 181 | path_str = entry.get("path") |
| 182 | if not path_str: |
| 183 | continue |
| 184 | sub_path = Path(path_str).resolve() |
| 185 | if sub_path.exists() and sub_path not in visited: |
| 186 | visited.add(sub_path) |
| 187 | stack.append(sub_path) |
| 188 | |
| 189 | # 2) Collect workflow modules referenced via `module: "modules/xxx.yaml"` |
| 190 | |
| 191 | def _iter_module_paths(node): |
| 192 | if isinstance(node, dict): |
| 193 | for k, v in node.items(): |
| 194 | if k == "module" and isinstance(v, str): |
| 195 | yield v |
| 196 | else: |
| 197 | yield from _iter_module_paths(v) |
| 198 | elif isinstance(node, list): |
| 199 | for item in node: |
| 200 | yield from _iter_module_paths(item) |
| 201 | |
| 202 | for module_rel in _iter_module_paths(yaml_data): |
| 203 | # Resolve module path using the same semantics as runtime: |
| 204 | # - absolute paths are used as-is |
| 205 | # - relative paths may be resolved using multiple strategies: |
| 206 | # * relative to the current file directory |
| 207 | # * relative to the root plan directory |
| 208 | # * under any sibling "<root_stem>_modules/**" directory (used by UI) |
| 209 | rel_obj = Path(module_rel) |
| 210 | candidates: Set[Path] = set() |
| 211 | if os.path.isabs(module_rel): |
no test coverage detected