|
| 1 | +import re |
| 2 | +from pathlib import Path |
| 3 | + |
| 4 | + |
| 5 | +def resolve_includes(yaml_content: str, *, base_dir: Path = Path(".")) -> str: |
| 6 | + """Pre-process YAML content to resolve all !include directives. |
| 7 | + This will take the yaml content as a string and return the same string with all |
| 8 | + !include directives resolved. |
| 9 | +
|
| 10 | + The reason we don't go with pyyaml-include or similar options is that they don't |
| 11 | + support merge keys, see https://github.com/tanbro/pyyaml-include/issues/53 . |
| 12 | + """ |
| 13 | + |
| 14 | + def include_replacer(match): |
| 15 | + indentation = match.group(1) # Capture the indentation |
| 16 | + prefix = match.group(2) # What comes before !include (like "<<: " or "key: ") |
| 17 | + include_path = match.group(3) |
| 18 | + full_path = base_dir / include_path |
| 19 | + |
| 20 | + included_content = full_path.read_text().strip() |
| 21 | + |
| 22 | + # Handle merge key specially |
| 23 | + if prefix.strip() == "<<:": |
| 24 | + # For merge keys, include the content with proper indentation |
| 25 | + if indentation: |
| 26 | + included_lines = included_content.splitlines() |
| 27 | + indented_lines = [indentation + line if line.strip() else line for line in included_lines] |
| 28 | + return indentation + "<<:\n" + "\n".join(indented_lines) |
| 29 | + else: |
| 30 | + # Indent the included content by 2 spaces for proper merge format |
| 31 | + included_lines = included_content.splitlines() |
| 32 | + indented_lines = [" " + line if line.strip() else line for line in included_lines] |
| 33 | + return "<<:\n" + "\n".join(indented_lines) |
| 34 | + else: |
| 35 | + # For regular includes, we need to handle indentation properly |
| 36 | + # If the prefix ends with ': ', we need to add a newline and indent the content |
| 37 | + if prefix.strip().endswith(":"): |
| 38 | + # Calculate the indentation for the included content |
| 39 | + content_indent = indentation + " " # Add 2 spaces for proper YAML nesting |
| 40 | + included_lines = included_content.splitlines() |
| 41 | + indented_lines = [content_indent + line if line.strip() else line for line in included_lines] |
| 42 | + return indentation + prefix.strip() + "\n" + "\n".join(indented_lines) |
| 43 | + else: |
| 44 | + # For cases where it's not a key assignment, just replace with content |
| 45 | + return indentation + prefix + included_content |
| 46 | + |
| 47 | + # Pattern to match !include directives with proper capture groups |
| 48 | + # Group 1: indentation, Group 2: prefix (like "<<: " or "key: "), Group 3: file path |
| 49 | + include_pattern = r"^(\s*)(.*?)!include\s+(.+)$" |
| 50 | + |
| 51 | + # Keep resolving includes until no more are found |
| 52 | + while re.search(include_pattern, yaml_content, re.MULTILINE): |
| 53 | + yaml_content = re.sub(include_pattern, include_replacer, yaml_content, flags=re.MULTILINE) |
| 54 | + |
| 55 | + return yaml_content |
0 commit comments