From f7b758a41d6ec81f9a3efaf6ba941543efc36299 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Wed, 21 Oct 2026 09:52:34 +0200 Subject: [PATCH] fix: patch batocera entries past commented items _patched_entry read '#{ ... }' lines as live items, found no match in the model and gave up, so atarist was written again whole (52 lines changed for 3 additions). A commented item is now left as written. --- scripts/exporter/batocera_exporter.py | 13 +++++++++++++ tests/test_exporters.py | 23 +++++++++++++++++++++-- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/scripts/exporter/batocera_exporter.py b/scripts/exporter/batocera_exporter.py index 0812b620..01b3e0b0 100644 --- a/scripts/exporter/batocera_exporter.py +++ b/scripts/exporter/batocera_exporter.py @@ -26,6 +26,15 @@ _ITEM = re.compile(r"\{[^{}\n]*\}") _MD5_VALUE = re.compile(r'("md5"\s*:\s*")[^"]*(")') +_STRING = re.compile(r'"(?:[^"\\\n]|\\.)*"') + + +def _commented(text: str, position: int) -> bool: + """Whether a '#' outside a string precedes `position` on its line.""" + line_start = text.rfind("\n", 0, position) + 1 + return "#" in _STRING.sub("", text[line_start:position]) + + def _note_key(text: str) -> tuple[str, str]: """(file, zippedFile) of the item a line holds, ("", "") for none.""" path = re.search(r'"file"\s*:\s*"([^"]+)"', text) @@ -316,6 +325,10 @@ class Exporter(BaseExporter): last: re.Match[str] | None = None consumed: dict[tuple, int] = {} for match in _ITEM.finditer(joined): + if _commented(joined, match.start()): + # '#{ "md5": ..., "file": "bios/tos102us.img"},' is a note + # Python never reads, and it stays as written. + continue try: theirs = ast.literal_eval(match.group(0)) except (SyntaxError, ValueError): diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 24e17ef4..0b039059 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -1919,7 +1919,7 @@ class BatoceraPatchesValuesNotEntries(unittest.TestCase): " return {}\n" ) - def render(self, truth_files): + def render(self, truth_files, original=None): scraped = {"cores": ["hatari"], "systems": {"atari-st": {"native_id": "atarist", "files": [ {"name": "tos102.img", "destination": "tos102.img", "native_system": "atarist", "native_name": "Atari ST", "native_path": "bios/tos102.img", "md5": "1" * 32}, @@ -1929,7 +1929,8 @@ class BatoceraPatchesValuesNotEntries(unittest.TestCase): systems, report = build_native_model({"systems": {"atari-st": {"files": truth_files}}}, scraped) exporter = discover_exporters()["batocera"]() - return exporter.render(systems, report, {"batocera-systems": self.ORIGINAL}, scraped)[ + original = original or self.ORIGINAL + return exporter.render(systems, report, {"batocera-systems": original}, scraped)[ "batocera-systems"] def test_a_corrected_md5_changes_that_value_only(self): @@ -1948,6 +1949,24 @@ class BatoceraPatchesValuesNotEntries(unittest.TestCase): self.assertIn("# ST\n", produced) self.assertIn('"file": "bios/tos102.img"},\n\n', produced) + def test_a_commented_item_does_not_stop_the_patch(self): + """atarist keeps seven TOS items behind '#'. Read as live items they + matched nothing in the model, and the entry was written again whole + for three additions.""" + commented = ' #{ "md5": "' + "5" * 32 + '", "file": "bios/tos102us.img"},\n' + original = self.ORIGINAL.replace("\n\n", "\n" + commented + "\n", 1) + produced = self.render([{"name": "tos206.img", "path": "tos206.img", + "md5": "4" * 32, "_cores": ["hatari"]}], original) + self.assertEqual( + produced, + original.replace( + '"file": "bios/tos104.img"}\n', + '"file": "bios/tos104.img"},\n' + ' { "md5": "' + "4" * 32 + '", "file": "bios/tos206.img",' + ' "emulator": "libretro", "core": "hatari"}\n', + ), + ) + class BatoceraExport(unittest.TestCase): def _render(self, originals):