diff.py 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294
  1. import os
  2. import subprocess
  3. import tempfile
  4. from contextlib import contextmanager
  5. from difflib import unified_diff
  6. from multiprocessing import Pool, cpu_count
  7. from typing import Any, Callable, Iterable, Iterator, List, Optional, Text, Tuple
  8. from fontTools.ttLib import TTFont # type: ignore
  9. from .utils import get_file_modtime
  10. #
  11. #
  12. # Private functions
  13. #
  14. #
  15. def _get_fonts_and_save_xml(
  16. filepath_a: Text,
  17. filepath_b: Text,
  18. tmpdirpath: Text,
  19. include_tables: Optional[List[Text]],
  20. exclude_tables: Optional[List[Text]],
  21. font_number_a: int,
  22. font_number_b: int,
  23. use_multiprocess: bool,
  24. ) -> Tuple[Text, Text, Text, Text, Text, Text]:
  25. post_pathname, postpath, pre_pathname, prepath = _get_pre_post_paths(
  26. filepath_a, filepath_b
  27. )
  28. # instantiate left and right fontTools.ttLib.TTFont objects
  29. tt_left = TTFont(prepath, fontNumber=font_number_a)
  30. tt_right = TTFont(postpath, fontNumber=font_number_b)
  31. left_ttxpath = os.path.join(tmpdirpath, "left.ttx")
  32. right_ttxpath = os.path.join(tmpdirpath, "right.ttx")
  33. _mp_save_ttx_xml(
  34. tt_left,
  35. tt_right,
  36. left_ttxpath,
  37. right_ttxpath,
  38. exclude_tables,
  39. include_tables,
  40. use_multiprocess,
  41. )
  42. return left_ttxpath, right_ttxpath, pre_pathname, prepath, post_pathname, postpath
  43. def _get_pre_post_paths(
  44. filepath_a: Text,
  45. filepath_b: Text,
  46. ) -> Tuple[Text, Text, Text, Text]:
  47. prepath = filepath_a
  48. postpath = filepath_b
  49. pre_pathname = filepath_a
  50. post_pathname = filepath_b
  51. return post_pathname, postpath, pre_pathname, prepath
  52. def _mp_save_ttx_xml(
  53. tt_left: Any,
  54. tt_right: Any,
  55. left_ttxpath: Text,
  56. right_ttxpath: Text,
  57. exclude_tables: Optional[List[Text]],
  58. include_tables: Optional[List[Text]],
  59. use_multiprocess: bool,
  60. ) -> None:
  61. if use_multiprocess and cpu_count() > 1:
  62. # Use parallel fontTools.ttLib.TTFont.saveXML dump
  63. # by default on multi CPU systems. This is a performance
  64. # optimization. Profiling demonstrates that this can reduce
  65. # execution time by up to 30% for some fonts
  66. mp_args_list = [
  67. (tt_left, left_ttxpath, include_tables, exclude_tables),
  68. (tt_right, right_ttxpath, include_tables, exclude_tables),
  69. ]
  70. with Pool(processes=2) as pool:
  71. pool.starmap(_ttfont_save_xml, mp_args_list)
  72. else:
  73. # use sequential fontTools.ttLib.TTFont.saveXML dumps
  74. # when use_multiprocess is False or single CPU system
  75. # detected
  76. _ttfont_save_xml(tt_left, left_ttxpath, include_tables, exclude_tables)
  77. _ttfont_save_xml(tt_right, right_ttxpath, include_tables, exclude_tables)
  78. def _ttfont_save_xml(
  79. ttf: Any,
  80. filepath: Text,
  81. include_tables: Optional[List[Text]],
  82. exclude_tables: Optional[List[Text]],
  83. ) -> bool:
  84. """Writes TTX specification formatted XML to disk on filepath."""
  85. ttf.saveXML(filepath, tables=include_tables, skipTables=exclude_tables)
  86. return True
  87. @contextmanager
  88. def _saved_ttx_files(
  89. filepath_a: Text,
  90. filepath_b: Text,
  91. include_tables: Optional[List[Text]],
  92. exclude_tables: Optional[List[Text]],
  93. font_number_a: int,
  94. font_number_b: int,
  95. use_multiprocess: bool,
  96. ) -> Iterator[Tuple[Text, Text, Text, Text, Text, Text]]:
  97. with tempfile.TemporaryDirectory() as tmpdirpath:
  98. yield _get_fonts_and_save_xml(
  99. filepath_a,
  100. filepath_b,
  101. tmpdirpath,
  102. include_tables,
  103. exclude_tables,
  104. font_number_a,
  105. font_number_b,
  106. use_multiprocess,
  107. )
  108. def _diff_with_saved_ttx_files(
  109. filepath_a: Text,
  110. filepath_b: Text,
  111. include_tables: Optional[List[Text]],
  112. exclude_tables: Optional[List[Text]],
  113. font_number_a: int,
  114. font_number_b: int,
  115. use_multiprocess: bool,
  116. create_differ: Callable[[Text, Text, Text, Text, Text, Text], Iterable[Text]],
  117. ) -> Iterator[Text]:
  118. with _saved_ttx_files(
  119. filepath_a,
  120. filepath_b,
  121. include_tables,
  122. exclude_tables,
  123. font_number_a,
  124. font_number_b,
  125. use_multiprocess,
  126. ) as (
  127. left_ttxpath,
  128. right_ttxpath,
  129. pre_pathname,
  130. prepath,
  131. post_pathname,
  132. postpath,
  133. ):
  134. yield from create_differ(
  135. left_ttxpath,
  136. right_ttxpath,
  137. pre_pathname,
  138. prepath,
  139. post_pathname,
  140. postpath,
  141. )
  142. #
  143. #
  144. # Public functions
  145. #
  146. #
  147. def u_diff(
  148. filepath_a: Text,
  149. filepath_b: Text,
  150. context_lines: int = 3,
  151. include_tables: Optional[List[Text]] = None,
  152. exclude_tables: Optional[List[Text]] = None,
  153. font_number_a: int = -1,
  154. font_number_b: int = -1,
  155. use_multiprocess: bool = True,
  156. ) -> Iterator[Text]:
  157. """Performs a unified diff on a TTX serialized data format dump of font binary data using
  158. a modified version of the Python standard libary difflib module.
  159. filepath_a: (string) pre-file local file path
  160. filepath_b: (string) post-file local file path
  161. context_lines: (int) number of context lines to include in the diff (default=3)
  162. include_tables: (list of str) Python list of OpenType tables to include in the diff
  163. exclude_tables: (list of str) Python list of OpentType tables to exclude from the diff
  164. use_multiprocess: (bool) use multi-processor optimizations (default=True)
  165. include_tables and exclude_tables are mutually exclusive arguments. Only one should
  166. be defined
  167. :returns: Generator of ordered diff line strings that include newline line endings
  168. :raises: KeyError if include_tables or exclude_tables includes a mis-specified table
  169. that is not included in filepath_a OR filepath_b
  170. """
  171. def _create_unified_diff(
  172. left_ttxpath: Text,
  173. right_ttxpath: Text,
  174. pre_pathname: Text,
  175. prepath: Text,
  176. post_pathname: Text,
  177. postpath: Text,
  178. ) -> Iterable[Text]:
  179. with open(left_ttxpath) as ff:
  180. fromlines = ff.readlines()
  181. with open(right_ttxpath) as tf:
  182. tolines = tf.readlines()
  183. fromdate = get_file_modtime(prepath)
  184. todate = get_file_modtime(postpath)
  185. yield from unified_diff(
  186. fromlines,
  187. tolines,
  188. pre_pathname,
  189. post_pathname,
  190. fromdate,
  191. todate,
  192. n=context_lines,
  193. )
  194. yield from _diff_with_saved_ttx_files(
  195. filepath_a,
  196. filepath_b,
  197. include_tables,
  198. exclude_tables,
  199. font_number_a,
  200. font_number_b,
  201. use_multiprocess,
  202. _create_unified_diff,
  203. )
  204. def run_external_diff(
  205. diff_tool: Text,
  206. diff_args: List[Text],
  207. filepath_a: Text,
  208. filepath_b: Text,
  209. include_tables: Optional[List[Text]] = None,
  210. exclude_tables: Optional[List[Text]] = None,
  211. font_number_a: int = -1,
  212. font_number_b: int = -1,
  213. use_multiprocess: bool = True,
  214. ) -> Iterator[Text]:
  215. """Performs a unified diff on a TTX serialized data format dump of font binary data using
  216. an external diff executable that is requested by the caller via `command`
  217. diff_tool: (string) command line executable string
  218. diff_args: (list of strings) arguments for the diff tool
  219. filepath_a: (string) pre-file local file path
  220. filepath_b: (string) post-file local file path
  221. include_tables: (list of str) Python list of OpenType tables to include in the diff
  222. exclude_tables: (list of str) Python list of OpentType tables to exclude from the diff
  223. use_multiprocess: (bool) use multi-processor optimizations (default=True)
  224. include_tables and exclude_tables are mutually exclusive arguments. Only one should
  225. be defined
  226. :returns: Generator of ordered diff line strings that include newline line endings
  227. :raises: KeyError if include_tables or exclude_tables includes a mis-specified table
  228. that is not included in filepath_a OR filepath_b
  229. :raises: IOError if exception raised during execution of `command` on TTX files
  230. """
  231. def _create_external_diff(
  232. left_ttxpath: Text,
  233. right_ttxpath: Text,
  234. _pre_pathname: Text,
  235. _prepath: Text,
  236. _post_pathname: Text,
  237. _postpath: Text,
  238. ) -> Iterable[Text]:
  239. command = [diff_tool] + diff_args + [left_ttxpath, right_ttxpath]
  240. process = subprocess.Popen(
  241. command,
  242. stdout=subprocess.PIPE,
  243. stderr=subprocess.PIPE,
  244. encoding="utf8",
  245. )
  246. for line in process.stdout:
  247. yield line
  248. err = process.stderr.read()
  249. if err:
  250. raise IOError(err)
  251. yield from _diff_with_saved_ttx_files(
  252. filepath_a,
  253. filepath_b,
  254. include_tables,
  255. exclude_tables,
  256. font_number_a,
  257. font_number_b,
  258. use_multiprocess,
  259. _create_external_diff,
  260. )