build_inline.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. #!/usr/bin/env python3
  2. """
  3. Build an inlined variant of mnemonicode.py with the wordlist embedded as a
  4. Python literal instead of loaded from wordlist.json at runtime.
  5. Usage:
  6. python3 build_inline.py [source.py] [wordlist.json] [output.py]
  7. Defaults:
  8. source mnemonicode.py
  9. wordlist wordlist.json
  10. output mnemonicode_inline.py
  11. """
  12. import json
  13. import sys
  14. import textwrap
  15. from pathlib import Path
  16. COLS = 79 # target line length for the word list
  17. # Markers that delimit the section to replace in the source file.
  18. IMPORT_JSON = "import json\n"
  19. IMPORT_PATH = "from pathlib import Path\n"
  20. WORDLIST_LINE = "_WORDLIST_JSON = Path(__file__).parent / \"wordlist.json\"\n"
  21. LOAD_OLD = '''\
  22. @lru_cache(maxsize=1)
  23. def _load() -> tuple[list[str], dict[str, int], str]:
  24. data = json.loads(_WORDLIST_JSON.read_text())
  25. words: list[str] = data["words"]
  26. index: dict[str, int] = {w: i for i, w in enumerate(words)}
  27. return words, index, data["version"]
  28. '''
  29. LOAD_NEW_TMPL = '''\
  30. _WORDLIST: tuple[list[str], str] = (
  31. {words_literal},
  32. {version_literal},
  33. )
  34. @lru_cache(maxsize=1)
  35. def _load() -> tuple[list[str], dict[str, int], str]:
  36. words, version = _WORDLIST
  37. index: dict[str, int] = {{w: i for i, w in enumerate(words)}}
  38. return words, index, version
  39. '''
  40. def format_words(words: list[str]) -> str:
  41. """Format a list of words as a Python list literal, wrapped at COLS."""
  42. items = [repr(w) for w in words]
  43. joined = ", ".join(items)
  44. # Fits on one line?
  45. one_line = f" [{joined}]"
  46. if len(one_line) <= COLS:
  47. return one_line
  48. # Wrap into multiple lines indented by 4 spaces inside the outer 2-space indent.
  49. lines = []
  50. current = ""
  51. for item in items:
  52. candidate = f"{current}, {item}" if current else item
  53. if len(candidate) + 4 > COLS - 2: # account for " [" prefix on first line
  54. if current:
  55. lines.append(current)
  56. current = item
  57. else:
  58. current = candidate
  59. if current:
  60. lines.append(current)
  61. inner = ",\n ".join(lines)
  62. return f" [\n {inner},\n ]"
  63. def build(source: Path, wordlist: Path, output: Path) -> None:
  64. src = source.read_text()
  65. data = json.loads(wordlist.read_text())
  66. words: list[str] = data["words"]
  67. version: str = data["version"]
  68. # Remove imports that are only needed for file-based loading.
  69. src = src.replace(IMPORT_JSON, "")
  70. src = src.replace(IMPORT_PATH, "")
  71. src = src.replace(WORDLIST_LINE + "\n", "") # remove trailing blank line too
  72. # Replace _load().
  73. words_lit = format_words(words)
  74. version_lit = repr(version)
  75. load_new = LOAD_NEW_TMPL.format(
  76. words_literal=words_lit,
  77. version_literal=version_lit,
  78. )
  79. if LOAD_OLD not in src:
  80. sys.exit(f"error: could not find _load() definition in {source}")
  81. src = src.replace(LOAD_OLD, load_new)
  82. output.write_text(src)
  83. print(f"wrote {output} ({output.stat().st_size:,} bytes)")
  84. if __name__ == "__main__":
  85. args = sys.argv[1:]
  86. source = Path(args[0]) if len(args) > 0 else Path("mnemonicode.py")
  87. wordlist = Path(args[1]) if len(args) > 1 else Path("wordlist.json")
  88. output = Path(args[2]) if len(args) > 2 else Path("mnemonicode_inline.py")
  89. for p in (source, wordlist):
  90. if not p.exists():
  91. sys.exit(f"error: {p} not found")
  92. build(source, wordlist, output)