topl
TOML extended with placeholders
#!/usr/bin/env -S uv run -s
/// script
dependencies = ["python-box", "rich", "fire"]
///
this_file: resolve_toml.py
""" resolve_toml.py
Resolve double‑curly‑brace placeholders in a TOML file in two phases:
- Internal phase – placeholders that reference keys inside the same
TOML structure are substituted first (e.g.
{{dict2.key2}}). - External phase – any remaining placeholders are substituted with
user‑supplied parameters (e.g.
external1="foo"). - Warning phase – unresolved placeholders are left intact and a warning is emitted.
The script purposefully performs minimal work: it does not try to
re‑order keys, merge files, or perform type conversions beyond str;
it only “does what it says on the tin”.
Usage (CLI)
./resolve_toml.py path/to/file.toml --external external1="bar" external2="baz"
The CLI is provided by fire; every keyword argument after the filename is treated as an external parameter.
⸻
Why Box?
Box gives intuitive dotted access (cfg.dict2.key2) while still behaving like a plain dict for serialization.
“””
from future import annotations
import logging import re import sys from pathlib import Path from types import MappingProxyType from typing import Any, Mapping
import tomllib # Python 3.11+ from box import Box import fire from rich.console import Console from rich.logging import RichHandler
—————————————————————————
Constants & regexes
_PLACEHOLDER_RE = re.compile(r”{{([^{}]+)}}”) _MAX_INTERNAL_PASSES = 10 # avoid infinite loops on circular refs
—————————————————————————
Logging setup – colourised & optionally verbose
def _configure_logging(verbose: bool = False) -> None: level = logging.DEBUG if verbose else logging.INFO logging.basicConfig( level=level, format=”%(message)s”, handlers=[RichHandler(rich_tracebacks=True, console=Console(stderr=True))], )
logger = logging.getLogger(name)
—————————————————————————
Low‑level helpers
def _get_by_path(box: Box, dotted_path: str) -> Any: “”” Return value at dotted_path or None if the path is invalid.
dotted_path follows Box semantics: "foo.bar.baz".
"""
current = box
for part in dotted_path.split("."):
if not isinstance(current, Mapping) or part not in current:
return None
current = current[part]
return current
def _resolve_internal_once(s: str, root: Box) -> str: “”” Replace one pass of internal placeholders in s.
A placeholder is internal if the path exists in root. """ def repl(match: re.Match[str]) -> str: path = match.group(1).strip() value = _get_by_path(root, path) return str(value) if value is not None else match.group(0)
return _PLACEHOLDER_RE.sub(repl, s)
def _resolve_external(s: str, params: Mapping[str, str]) -> str: “”” Replace external placeholders using str.format_map.
We temporarily convert {{name}} → {name} then format.
Missing keys are left untouched.
"""
class _SafeDict(dict): # noqa: D401 """dict that leaves unknown placeholders unchanged."""
def __missing__(self, key: str) -> str: # noqa: D401
return f"{{{{{key}}}}}"
if not params: return s
Convert {{name}} → {name}
tmp = _PLACEHOLDER_RE.sub(lambda m: "{" + m.group(1).strip() + "}", s) return tmp.format_map(_SafeDict(params))
def _iter_box_strings(box: Box) -> tuple[tuple[str, Box], …]: “”” Yield (key, parent_box) pairs for every string leaf in box.
We return both key and the parent so we can assign new values in‑place. """ results: list[tuple[str, Box]] = [] for key, val in box.items(): if isinstance(val, str): results.append((key, box)) elif isinstance(val, Mapping): results.extend(_iter_box_strings(val)) # type: ignore[arg-type] return tuple(results)
—————————————————————————
Public API
def resolve_placeholders(data: Mapping[str, Any], **params: str) -> Box: “”” Resolve placeholders inside data in‑place and return a new Box.
Parameters
data:
Mapping returned by tomllib.load.
**params:
External parameters used during the external phase.
Returns
Box The resolved configuration object. """ cfg = Box(data, default_box=True, default_box_attr=None)
-- Phase 1: internal substitutions (multiple passes) ------------------
for i in range(_MAX_INTERNAL_PASSES): changed = False for key, parent in _iter_box_strings(cfg): original = parent[key] resolved = _resolve_internal_once(original, cfg) if original != resolved: parent[key] = resolved changed = True if not changed: logger.debug("Internal resolution stabilised after %s passes", i + 1) break else: # pragma: no cover logger.warning( "Reached maximum internal passes (%s). " "Possible circular placeholder references?", _MAX_INTERNAL_PASSES, )
-- Phase 2: external substitutions -----------------------------------
for key, parent in _iter_box_strings(cfg): parent[key] = _resolve_external(parent[key], MappingProxyType(params))
-- Phase 3: warn about leftovers -------------------------------------
leftovers: list[str] = [] for key, parent in _iter_box_strings(cfg): for match in _PLACEHOLDER_RE.finditer(parent[key]): leftovers.append(match.group(0)) if leftovers: unique = sorted(set(leftovers)) logger.warning( "Could not resolve %s placeholder(s): %s", len(unique), ", ".join(unique), )
return cfg
—————————————————————————
CLI entry‑point
def main(path: str, verbose: bool = False, **params: str) -> None: # noqa: D401 “”” Read path (TOML), resolve placeholders, and pretty‑print the result.
Any key=value arguments after path are considered external params.
"""
_configure_logging(verbose)
toml_path = Path(path).expanduser() try: data = toml_path.read_bytes() except FileNotFoundError: logger.error("TOML file %s not found", toml_path) sys.exit(1)
config = resolve_placeholders(tomllib.loads(data.decode()), **params) Console().print(config.to_dict())
if name == “main”: # pragma: no cover fire.Fire(main)
How this fulfils the brief 📝
- Two‑phase resolution:
Internal references are substituted first; only the unresolved placeholders are then offered to external parameters viastr.format_map. - Warnings: Any placeholders still unreplaced are logged once – exactly as requested.
- Box integration: The Toml structure is returned as a
Box, so callers keep dotted access for further processing. - CLI optionality: Fire provides a one‑liner interface but is not mandatory for library use.
- Safety: Circular references are detected via a pass‑count limit and will not hang the program.
Feel free to drop the CLI bits if you only need a function – everything is modular.
Metadata
Release files for toml-topl 1.0.5
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| toml_topl-1.0.5.tar.gz | 19.6 kB | Without provenance | Details for toml_topl-1.0.5.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| toml_topl-1.0.5-py3-none-any.whl | Python 3 | none | any | Details for toml_topl-1.0.5-py3-none-any.whl |
Total release size: 33.3 kB
Release files / toml_topl-1.0.5.tar.gz
| Download URL | toml_topl-1.0.5.tar.gz |
|---|---|
| Size | 19.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
567c5e15159bfd6ff0750735e7c768de8d96a038f9ce76707a3c602e076dd067
|
|
BLAKE2b-256 checksum How to use checksums |
eec406699a8648986b0a954188f344689d5f97517676957a4f812748b168aab8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
python-httpx/0.28.1
|
Release files / toml_topl-1.0.5-py3-none-any.whl
| Download URL | toml_topl-1.0.5-py3-none-any.whl |
|---|---|
| Size | 13.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
79cdfc3ad4cccdde211b78f6ea5c499e30c18ad7f5316c5a7a73153ce81554f4
|
|
BLAKE2b-256 checksum How to use checksums |
0f89c031801b51a4a74e54cc9720842d009f850f392cf86362cbf7744d18371d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
python-httpx/0.28.1
|
Security for toml-topl 1.0.5
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.