#!/usr/bin/env python3 """ Build an inlined variant of mnemonicode.py with the wordlist embedded as a Python literal instead of loaded from wordlist.json at runtime. Usage: python3 build_inline.py [source.py] [wordlist.json] [output.py] Defaults: source mnemonicode.py wordlist wordlist.json output mnemonicode_inline.py """ import json import sys import textwrap from pathlib import Path COLS = 79 # target line length for the word list # Markers that delimit the section to replace in the source file. IMPORT_JSON = "import json\n" IMPORT_PATH = "from pathlib import Path\n" WORDLIST_LINE = "_WORDLIST_JSON = Path(__file__).parent / \"wordlist.json\"\n" LOAD_OLD = '''\ @lru_cache(maxsize=1) def _load() -> tuple[list[str], dict[str, int], str]: data = json.loads(_WORDLIST_JSON.read_text()) words: list[str] = data["words"] index: dict[str, int] = {w: i for i, w in enumerate(words)} return words, index, data["version"] ''' LOAD_NEW_TMPL = '''\ _WORDLIST: tuple[list[str], str] = ( {words_literal}, {version_literal}, ) @lru_cache(maxsize=1) def _load() -> tuple[list[str], dict[str, int], str]: words, version = _WORDLIST index: dict[str, int] = {{w: i for i, w in enumerate(words)}} return words, index, version ''' def format_words(words: list[str]) -> str: """Format a list of words as a Python list literal, wrapped at COLS.""" items = [repr(w) for w in words] joined = ", ".join(items) # Fits on one line? one_line = f" [{joined}]" if len(one_line) <= COLS: return one_line # Wrap into multiple lines indented by 4 spaces inside the outer 2-space indent. lines = [] current = "" for item in items: candidate = f"{current}, {item}" if current else item if len(candidate) + 4 > COLS - 2: # account for " [" prefix on first line if current: lines.append(current) current = item else: current = candidate if current: lines.append(current) inner = ",\n ".join(lines) return f" [\n {inner},\n ]" def build(source: Path, wordlist: Path, output: Path) -> None: src = source.read_text() data = json.loads(wordlist.read_text()) words: list[str] = data["words"] version: str = data["version"] # Remove imports that are only needed for file-based loading. src = src.replace(IMPORT_JSON, "") src = src.replace(IMPORT_PATH, "") src = src.replace(WORDLIST_LINE + "\n", "") # remove trailing blank line too # Replace _load(). words_lit = format_words(words) version_lit = repr(version) load_new = LOAD_NEW_TMPL.format( words_literal=words_lit, version_literal=version_lit, ) if LOAD_OLD not in src: sys.exit(f"error: could not find _load() definition in {source}") src = src.replace(LOAD_OLD, load_new) output.write_text(src) print(f"wrote {output} ({output.stat().st_size:,} bytes)") if __name__ == "__main__": args = sys.argv[1:] source = Path(args[0]) if len(args) > 0 else Path("mnemonicode.py") wordlist = Path(args[1]) if len(args) > 1 else Path("wordlist.json") output = Path(args[2]) if len(args) > 2 else Path("mnemonicode_inline.py") for p in (source, wordlist): if not p.exists(): sys.exit(f"error: {p} not found") build(source, wordlist, output)