| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- #!/usr/bin/env python3
- """
- Standalone reader for .trellis/config.yaml.
- Mirrors a minimal subset of common.config so callers (hooks, workflow_phase)
- can read configuration without importing the full task/repo helpers. Returns
- an empty dict on missing/malformed files so callers stay simple.
- """
- from __future__ import annotations
- from pathlib import Path
- from typing import Optional
- CONFIG_REL_PATH = ".trellis/config.yaml"
- def _unquote(value: str) -> str:
- if len(value) >= 2 and value[0] == value[-1] and value[0] in ('"', "'"):
- return value[1:-1]
- return value
- def _strip_inline_comment(value: str) -> str:
- """Strip ` # …` inline comments while preserving `#` inside quoted strings.
- YAML treats ` #` (space-hash) as a comment opener; bare `#` inside a token
- is part of the value. Quoted strings are immune.
- """
- in_quote: str | None = None
- for idx, ch in enumerate(value):
- if in_quote:
- if ch == in_quote:
- in_quote = None
- continue
- if ch in ('"', "'"):
- in_quote = ch
- continue
- if ch == "#" and (idx == 0 or value[idx - 1].isspace()):
- return value[:idx]
- return value
- def _next_content_line(lines: list[str], start: int) -> tuple[int, str]:
- i = start
- while i < len(lines):
- stripped = lines[i].strip()
- if stripped and not stripped.startswith("#"):
- return i, lines[i]
- i += 1
- return i, ""
- def _parse_yaml_block(
- lines: list[str], start: int, min_indent: int, target: dict
- ) -> int:
- i = start
- current_list: list | None = None
- while i < len(lines):
- line = lines[i]
- stripped = line.strip()
- if not stripped or stripped.startswith("#"):
- i += 1
- continue
- indent = len(line) - len(line.lstrip())
- if indent < min_indent:
- break
- if stripped.startswith("- "):
- if current_list is not None:
- current_list.append(_unquote(stripped[2:].strip()))
- i += 1
- elif ":" in stripped:
- key, _, value = stripped.partition(":")
- key = key.strip()
- value = _strip_inline_comment(value).strip()
- value = _unquote(value)
- current_list = None
- if value:
- target[key] = value
- i += 1
- else:
- next_i, next_line = _next_content_line(lines, i + 1)
- if next_i >= len(lines):
- target[key] = {}
- i = next_i
- elif next_line.strip().startswith("- "):
- current_list = []
- target[key] = current_list
- i += 1
- else:
- next_indent = len(next_line) - len(next_line.lstrip())
- if next_indent > indent:
- nested: dict = {}
- target[key] = nested
- i = _parse_yaml_block(lines, i + 1, next_indent, nested)
- else:
- target[key] = {}
- i += 1
- else:
- i += 1
- return i
- def parse_simple_yaml(content: str) -> dict:
- """Parse a small subset of YAML. See common.config for full doc."""
- lines = content.splitlines()
- result: dict = {}
- _parse_yaml_block(lines, 0, 0, result)
- return result
- def read_trellis_config(repo_root: Optional[Path] = None) -> dict:
- """Read .trellis/config.yaml. Returns {} on missing or malformed file."""
- root = repo_root or Path.cwd()
- config_file = root / CONFIG_REL_PATH
- try:
- content = config_file.read_text(encoding="utf-8")
- except (FileNotFoundError, OSError):
- return {}
- try:
- parsed = parse_simple_yaml(content)
- except Exception:
- return {}
- return parsed if isinstance(parsed, dict) else {}
|