Source code for batter.config.utils

from __future__ import annotations

import os
import re
from pathlib import Path
from typing import Any, Mapping, Sequence


_SANITIZE_RE = re.compile(r"[^A-Za-z0-9_]+")
_RESERVED_LIGAND_NAMES = frozenset({"TRANSFORMATIONS"})


[docs] def coerce_yes_no(value: Any) -> str | None: """ Normalize boolean-like values into ``\"yes\"`` or ``\"no\"``. Parameters ---------- value : Input flag provided by the user. Supported types include ``bool``, numeric scalars, or strings such as ``\"true\"`` and ``\"0\"``. Returns ------- str or None ``\"yes\"`` or ``\"no\"`` when the flag can be interpreted. ``None`` is returned unchanged to preserve optional semantics. Raises ------ ValueError If the value cannot be coerced into a boolean switch. """ if value is None: return None if isinstance(value, bool): return "yes" if value else "no" if isinstance(value, (int, float)): return "yes" if value else "no" if isinstance(value, str): text = value.strip().lower() if text in {"yes", "no"}: return text if text in {"true", "t", "1"}: return "yes" if text in {"false", "f", "0"}: return "no" raise ValueError(f"Expected yes/no (or boolean), got {value!r}")
[docs] def sanitize_ligand_name(name: str) -> str: """ Convert a ligand identifier into a filesystem-safe token. Parameters ---------- name : str Original ligand identifier, often derived from filenames or keys. Returns ------- str Uppercase alphanumeric token with unsafe characters replaced by underscores. """ # Ensure pair delimiters are preserved as underscores before regex cleanup. cleaned = name.replace("~", "_").strip() cleaned = _SANITIZE_RE.sub("_", cleaned) return cleaned.strip("_").upper()
[docs] def sanitize_user_ligand_name(name: str) -> str: """ Sanitize and validate a user-provided ligand identifier. Reserved names that conflict with BATTER directory layout are rejected. """ sanitized = sanitize_ligand_name(name) if not sanitized: raise ValueError(f"Ligand name {name!r} is invalid after sanitization.") if sanitized in _RESERVED_LIGAND_NAMES: raise ValueError( f"Ligand name {name!r} is reserved. Please choose a different ligand name." ) return sanitized
[docs] def normalize_optional_path(value: Any) -> Path | None: """ Resolve optional path-like values into :class:`pathlib.Path` objects. Parameters ---------- value : Path candidate that may be ``None`` or an empty string. Strings may contain environment variables or ``~``. Returns ------- pathlib.Path or None Expanded path when provided; ``None`` if the value is empty. """ if value in (None, ""): return None return Path(os.path.expanduser(os.path.expandvars(str(value))))
[docs] def expand_env_vars(data: Any, *, base_dir: Path | None = None) -> Any: """ Recursively expand environment variables in a YAML-derived structure. Parameters ---------- data : Parsed YAML content to normalise. base_dir : Path, optional Base directory for resolving relative (``./``) paths. Returns ------- Any Structure with string values expanded. """ def _expand(value: Any) -> Any: if isinstance(value, str): expanded = os.path.expandvars(os.path.expanduser(value)) return expanded if isinstance(value, Mapping): return {k: _expand(v) for k, v in value.items()} if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)): return [_expand(v) for v in value] return value return _expand(data)