"""Stdlib reader for the block-YAML subset the bootstrap SPOT files use. This is a deliberate second YAML touchpoint outside ``utils.cache.yaml``: the modules that read the distro SPOT must import on the bare bootstrap python, which runs the ``.env`` generator before any dependency is installed, so they cannot reach the PyYAML-backed cache. Supported: nested block mappings, block sequences of scalars, plain and double-quoted string scalars, full-line and trailing comments, a single leading document marker. Every other construct raises :class:`BootstrapYamlError`, so ``load_block(text)`` either equals ``yaml.safe_load(text)`` or fails loud. The plain-scalar rule is deliberately narrower than YAML's: a plain scalar must start with a letter, an underscore or a slash, and may then carry only ``[A-Za-z0-9_./+@:-]`` and spaces. That excludes every leading character PyYAML's implicit resolvers key off, so no accepted plain scalar can come back as an int, float, bool, timestamp or null. """ from __future__ import annotations import re _PLAIN = re.compile(r"^[A-Za-z_/][A-Za-z0-9_./+@:\- ]*$") _RESERVED = re.compile(r"^(?:true|false|yes|no|on|off|y|n|null)$", re.IGNORECASE) _NON_PRINTABLE = re.compile( r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x84\x86-\x9f\ud800-\udfff￾￿]" ) class BootstrapYamlError(ValueError): """Raised for a construct outside the supported block subset.""" def _plain(text: str, lineno: int) -> str: if not _PLAIN.match(text) or ": " in text or text.endswith(":"): raise BootstrapYamlError(f"line {lineno}: unsupported plain scalar {text!r}") if _RESERVED.match(text): raise BootstrapYamlError(f"line {lineno}: {text!r} is not a string, quote it") return text def _scalar(raw: str, lineno: int) -> str: if raw.startswith('"'): end = raw.find('"', 1) if end < 0 or "\\" in raw[1:end]: raise BootstrapYamlError( f"line {lineno}: unsupported quoted scalar {raw!r}" ) trailer = raw[end + 1 :].strip() if trailer and not trailer.startswith("#"): raise BootstrapYamlError(f"line {lineno}: trailing text after {raw!r}") return raw[1:end] return _plain(raw.split(" #", 1)[0].strip(), lineno) def load_block(text: str) -> dict: """Parse *text* as a block mapping and return it. Args: text: YAML source restricted to the supported block subset. Returns: The root mapping, with every scalar as a ``str``. Raises: BootstrapYamlError: for any construct outside the subset. """ non_printable = _NON_PRINTABLE.search(text) if non_printable: raise BootstrapYamlError( f"non-printable character {non_printable.group()!r} at offset {non_printable.start()}" ) root: dict = {} stack: list[tuple[int, object]] = [(0, root)] pending: tuple[dict, str, int] | None = None for lineno, line in enumerate(text.splitlines(), 1): if "\t" in line: raise BootstrapYamlError(f"line {lineno}: tab indentation") body = line.strip() if not body or body.startswith("#"): continue if body == "---": if lineno != 1: raise BootstrapYamlError(f"line {lineno}: second document") if line.startswith(" "): raise BootstrapYamlError(f"line {lineno}: indented document marker") continue indent = len(line) - len(line.lstrip(" ")) if pending is not None: parent, key, parent_indent = pending if indent <= parent_indent: raise BootstrapYamlError( f"line {lineno}: {key!r} has neither a value nor a block" ) child: object = [] if body.startswith("- ") else {} parent[key] = child stack.append((indent, child)) pending = None else: while indent < stack[-1][0]: stack.pop() if indent != stack[-1][0]: raise BootstrapYamlError(f"line {lineno}: unexpected indent") container = stack[-1][1] if body.startswith("- "): if not isinstance(container, list): raise BootstrapYamlError(f"line {lineno}: sequence entry in a mapping") container.append(_scalar(body[2:], lineno)) continue if isinstance(container, list): raise BootstrapYamlError(f"line {lineno}: mapping entry in a sequence") key, sep, rest = body.partition(": ") if not sep: if not body.endswith(":"): raise BootstrapYamlError( f"line {lineno}: not a mapping entry: {body!r}" ) key, rest = body[:-1], "" if key != key.strip(): raise BootstrapYamlError(f"line {lineno}: not a mapping entry: {body!r}") _plain(key, lineno) if key in container: raise BootstrapYamlError(f"line {lineno}: duplicate key {key!r}") rest = rest.strip() if not rest or rest.startswith("#"): pending = (container, key, indent) continue container[key] = _scalar(rest, lineno) if pending is not None: raise BootstrapYamlError(f"{pending[1]!r} has neither a value nor a block") if not root: raise BootstrapYamlError("empty document") return root