Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions platformio/project/_ini_preserve.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
from __future__ import annotations
from pathlib import Path
from typing import Dict, Tuple

def _normalize_changes(changes: Dict[Tuple[str, str], str]):
# accept {("section","key"): "value"} or {"section.key": "value"}
norm = {}
for k, v in changes.items():
if isinstance(k, tuple) and len(k) == 2:
sec, key = k
else:
parts = str(k).split(".", 1)
if len(parts) != 2:
raise ValueError(f"invalid change key: {k!r}")
sec, key = parts
norm[(str(sec), str(key))] = v
return norm

def save_ini_preserving_comments(path: str | Path, changes: Dict, encoding: str = "utf-8") -> bool:
"""
Try to apply key/value 'changes' to INI at 'path' while preserving comments
and formatting. Returns True on success. If anything goes wrong, returns
False so callers can fall back to the legacy writer.
"""
path = Path(path)
changes = _normalize_changes(changes)

# Attempt preferred path: ConfigUpdater (preserves comments/ordering)
try:
from configupdater import ConfigUpdater # type: ignore
upd = ConfigUpdater()
upd.read(path, encoding=encoding)

for (sec, key), value in changes.items():
if not upd.has_section(sec):
upd.add_section(sec)
# Only touch the exact key; do not rewrite entire section.
if key in upd[sec]:
if upd[sec][key].value != str(value):
upd[sec][key].value = str(value)
else:
# Insert at the end of the section so we don't disturb existing lines.
upd[sec].add_after(upd[sec][-1].key if len(upd[sec]) else None, key, str(value))

with path.open("w", encoding=encoding, newline="\n") as f:
upd.write(f)
return True
except Exception:
# Any error here should fall through to legacy path
pass

# Fallback: standard configparser (will lose comments)
try:
import configparser
parser = configparser.ConfigParser(interpolation=None)
# Read existing values but comments will be lost on write
with path.open("r", encoding=encoding, newline="") as f:
parser.read_file(f)
for (sec, key), value in changes.items():
if not parser.has_section(sec) and sec.lower() != "default":
parser.add_section(sec)
if sec.lower() == "default":
parser.set("DEFAULT", key, str(value))
else:
parser.set(sec, key, str(value))
with path.open("w", encoding=encoding, newline="\n") as f:
parser.write(f)
return True
except Exception:
return False
22 changes: 22 additions & 0 deletions scripts/ini_preserve_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from pathlib import Path
import sys
from platformio.project._ini_preserve import save_ini_preserving_comments

def main():
if len(sys.argv) < 4 or len(sys.argv) % 2 != 0:
print("Usage: python scripts/ini_preserve_demo.py <ini_path> <section.key> <value> [<section.key> <value> ...]")
sys.exit(2)

ini = Path(sys.argv[1])
pairs = sys.argv[2:]
changes = {}
for i in range(0, len(pairs), 2):
k = pairs[i]
v = pairs[i+1]
changes[k] = v

ok = save_ini_preserving_comments(ini, changes)
print("OK" if ok else "FAILED")

if __name__ == "__main__":
main()
36 changes: 36 additions & 0 deletions tests/project/test_ini_preserve_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from pathlib import Path
from platformio.project._ini_preserve import save_ini_preserving_comments

SAMPLE = """\
; header comment
# second comment
[platformio]
description = Demo ; keep inline

[env:demo]
platform = espressif32
framework = arduino ; inline stays
lib_deps =
; a list kept as comments
; me-no-dev/AsyncTCP @ ^1.1.1
; tail comment
"""

def test_preserve_comments(tmp_path: Path):
ini = tmp_path / "platformio.ini"
ini.write_text(SAMPLE, encoding="utf-8", newline="\n")

ok = save_ini_preserving_comments(
ini,
{("env:demo", "platform"): "[email protected]"}
)
assert ok, "helper should succeed"

out = ini.read_text(encoding="utf-8")
assert "platform = [email protected]" in out
assert "; header comment" in out
assert "# second comment" in out
assert "description = Demo ; keep inline" in out
assert "framework = arduino ; inline stays" in out
assert "; me-no-dev/AsyncTCP @ ^1.1.1" in out
assert "; tail comment" in out