From cef6d943e2d2f7a09a18c1660a6fde8c82afe772 Mon Sep 17 00:00:00 2001 From: Abdessamad Derraz <3028866+Abdess@users.noreply.github.com> Date: Tue, 20 Oct 2026 01:22:27 +0200 Subject: [PATCH] fix: a Batocera note stays with its own item Notes were keyed by file path, so a comment before one of the items reading an archive came back before every item reading it: bk's '# MAME' twelve times. A comment after the list's opening bracket matched neither rule and was dropped (enterprise's ep128emu-core). Notes are now keyed by file and archive member, written once, and a head-line comment goes with the first item. --- scripts/exporter/batocera_exporter.py | 44 ++++++++++++++++----------- tests/test_exporters.py | 20 ++++++++++++ 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/scripts/exporter/batocera_exporter.py b/scripts/exporter/batocera_exporter.py index a7a2a51c..55e6ad6b 100644 --- a/scripts/exporter/batocera_exporter.py +++ b/scripts/exporter/batocera_exporter.py @@ -23,6 +23,13 @@ _ENTRY_START = re.compile(r'^(\s{4})"([^"]+)":\s*\{') _INDENT = " " * 4 +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) + member = re.search(r'"zippedFile"\s*:\s*"([^"]+)"', text) + return (path.group(1) if path else "", member.group(1) if member else "") + + class Exporter(BaseExporter): """Write Batocera's batocera-systems, corrected.""" @@ -208,16 +215,22 @@ class Exporter(BaseExporter): ) @staticmethod - def _item_notes(lines: list[str]) -> tuple[dict[str, str], dict[str, list[str]]]: - """The notes a maintainer wrote about each file, keyed by its path. + def _item_notes( + lines: list[str], + ) -> tuple[dict[tuple[str, str], str], dict[tuple[str, str], list[str]]]: + """The notes a maintainer wrote about each item, keyed by its file + and the member it names inside an archive. Rewriting an entry as one line would drop them, and a note like "ideally - 94bc50..." is the only record of why a hash is blank. - A comment on its own line belongs to the file below it, a comment at - the end of a line belongs to the file on it. + A comment on its own line belongs to the item below it, a comment at + the end of a line belongs to the item on it, and a comment after the + list's opening bracket (`"biosFiles": [# ep128emu-core`) to the first + item. Keyed by path alone, bk's '# MAME' came back before each of + twelve items reading bios/bk0010.zip. """ - trailing: dict[str, str] = {} - leading: dict[str, list[str]] = {} + trailing: dict[tuple[str, str], str] = {} + leading: dict[tuple[str, str], list[str]] = {} pending: list[str] = [] for line in lines: stripped = line.strip() @@ -225,17 +238,19 @@ class Exporter(BaseExporter): if stripped.startswith("#"): pending.append(stripped) continue - path = re.search(r'"file"\s*:\s*"([^"]+)"', line) - if not path: + key = _note_key(line) + if not key[0]: + if 0 <= hash_at and "[" in line[:hash_at]: + pending.append(line[hash_at:].strip()) continue - key = path.group(1) if pending: leading[key] = pending pending = [] - if 0 <= hash_at and hash_at > line.index(key): + if 0 <= hash_at and hash_at > line.index(key[0]): trailing[key] = line[hash_at:].rstrip() return trailing, leading + def _entry_block( self, system: NativeSystem, @@ -259,8 +274,8 @@ class Exporter(BaseExporter): pad = " " * (len(_INDENT) + 4) lines = [head] for index, item in enumerate(items): - key = self._item_path(item) - lines.extend(f"{pad}{note}" for note in leading.get(key, [])) + key = _note_key(item) + lines.extend(f"{pad}{note}" for note in leading.pop(key, [])) separator = "," if index < len(items) - 1 else "" note = trailing.get(key, "") suffix = f" {note}" if note else "" @@ -268,11 +283,6 @@ class Exporter(BaseExporter): lines.append(f"{_INDENT}] }},") return lines - @staticmethod - def _item_path(item: str) -> str: - match = re.search(r'"file": "([^"]+)"', item) - return match.group(1) if match else "" - @staticmethod def _split_original(original: str) -> tuple[list[str], list[str], list[str]]: """Cut the script into what precedes the dict, the dict, what follows.""" diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 9834f56f..9f496447 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -1744,5 +1744,25 @@ class RetroDeckAdditionsFollowTheirCore(unittest.TestCase): self.assertEqual(read_by_armsx2.native("component", ""), "") +class BatoceraNotesStayWithTheirItem(unittest.TestCase): + """Keyed by path, bk's '# MAME' came back before each of twelve items + reading bios/bk0010.zip, and enterprise's '"biosFiles": [# ep128emu-core' + was dropped.""" + + def test_notes_are_kept_once_per_item(self): + from exporter.batocera_exporter import Exporter + + lines = [ + ' "bk": { "name": "BK", "biosFiles": [# head note', + " # MAME", + ' { "md5": "a", "file": "bios/bk0010.zip", "zippedFile": "one.rom" },', + ' { "md5": "b", "file": "bios/bk0010.zip", "zippedFile": "two.rom" }, # tail', + " ] },", + ] + trailing, leading = Exporter._item_notes(lines) + self.assertEqual(leading, {("bios/bk0010.zip", "one.rom"): ["# head note", "# MAME"]}) + self.assertEqual(trailing, {("bios/bk0010.zip", "two.rom"): "# tail"}) + + if __name__ == "__main__": unittest.main()