import os import subprocess import tempfile from contextlib import contextmanager from difflib import unified_diff from multiprocessing import Pool, cpu_count from typing import Any, Callable, Iterable, Iterator, List, Optional, Text, Tuple from fontTools.ttLib import TTFont # type: ignore from .utils import get_file_modtime # # # Private functions # # def _get_fonts_and_save_xml( filepath_a: Text, filepath_b: Text, tmpdirpath: Text, include_tables: Optional[List[Text]], exclude_tables: Optional[List[Text]], font_number_a: int, font_number_b: int, use_multiprocess: bool, ) -> Tuple[Text, Text, Text, Text, Text, Text]: post_pathname, postpath, pre_pathname, prepath = _get_pre_post_paths( filepath_a, filepath_b ) # instantiate left and right fontTools.ttLib.TTFont objects tt_left = TTFont(prepath, fontNumber=font_number_a) tt_right = TTFont(postpath, fontNumber=font_number_b) left_ttxpath = os.path.join(tmpdirpath, "left.ttx") right_ttxpath = os.path.join(tmpdirpath, "right.ttx") _mp_save_ttx_xml( tt_left, tt_right, left_ttxpath, right_ttxpath, exclude_tables, include_tables, use_multiprocess, ) return left_ttxpath, right_ttxpath, pre_pathname, prepath, post_pathname, postpath def _get_pre_post_paths( filepath_a: Text, filepath_b: Text, ) -> Tuple[Text, Text, Text, Text]: prepath = filepath_a postpath = filepath_b pre_pathname = filepath_a post_pathname = filepath_b return post_pathname, postpath, pre_pathname, prepath def _mp_save_ttx_xml( tt_left: Any, tt_right: Any, left_ttxpath: Text, right_ttxpath: Text, exclude_tables: Optional[List[Text]], include_tables: Optional[List[Text]], use_multiprocess: bool, ) -> None: if use_multiprocess and cpu_count() > 1: # Use parallel fontTools.ttLib.TTFont.saveXML dump # by default on multi CPU systems. This is a performance # optimization. Profiling demonstrates that this can reduce # execution time by up to 30% for some fonts mp_args_list = [ (tt_left, left_ttxpath, include_tables, exclude_tables), (tt_right, right_ttxpath, include_tables, exclude_tables), ] with Pool(processes=2) as pool: pool.starmap(_ttfont_save_xml, mp_args_list) else: # use sequential fontTools.ttLib.TTFont.saveXML dumps # when use_multiprocess is False or single CPU system # detected _ttfont_save_xml(tt_left, left_ttxpath, include_tables, exclude_tables) _ttfont_save_xml(tt_right, right_ttxpath, include_tables, exclude_tables) def _ttfont_save_xml( ttf: Any, filepath: Text, include_tables: Optional[List[Text]], exclude_tables: Optional[List[Text]], ) -> bool: """Writes TTX specification formatted XML to disk on filepath.""" ttf.saveXML(filepath, tables=include_tables, skipTables=exclude_tables) return True @contextmanager def _saved_ttx_files( filepath_a: Text, filepath_b: Text, include_tables: Optional[List[Text]], exclude_tables: Optional[List[Text]], font_number_a: int, font_number_b: int, use_multiprocess: bool, ) -> Iterator[Tuple[Text, Text, Text, Text, Text, Text]]: with tempfile.TemporaryDirectory() as tmpdirpath: yield _get_fonts_and_save_xml( filepath_a, filepath_b, tmpdirpath, include_tables, exclude_tables, font_number_a, font_number_b, use_multiprocess, ) def _diff_with_saved_ttx_files( filepath_a: Text, filepath_b: Text, include_tables: Optional[List[Text]], exclude_tables: Optional[List[Text]], font_number_a: int, font_number_b: int, use_multiprocess: bool, create_differ: Callable[[Text, Text, Text, Text, Text, Text], Iterable[Text]], ) -> Iterator[Text]: with _saved_ttx_files( filepath_a, filepath_b, include_tables, exclude_tables, font_number_a, font_number_b, use_multiprocess, ) as ( left_ttxpath, right_ttxpath, pre_pathname, prepath, post_pathname, postpath, ): yield from create_differ( left_ttxpath, right_ttxpath, pre_pathname, prepath, post_pathname, postpath, ) # # # Public functions # # def u_diff( filepath_a: Text, filepath_b: Text, context_lines: int = 3, include_tables: Optional[List[Text]] = None, exclude_tables: Optional[List[Text]] = None, font_number_a: int = -1, font_number_b: int = -1, use_multiprocess: bool = True, ) -> Iterator[Text]: """Performs a unified diff on a TTX serialized data format dump of font binary data using a modified version of the Python standard libary difflib module. filepath_a: (string) pre-file local file path filepath_b: (string) post-file local file path context_lines: (int) number of context lines to include in the diff (default=3) include_tables: (list of str) Python list of OpenType tables to include in the diff exclude_tables: (list of str) Python list of OpentType tables to exclude from the diff use_multiprocess: (bool) use multi-processor optimizations (default=True) include_tables and exclude_tables are mutually exclusive arguments. Only one should be defined :returns: Generator of ordered diff line strings that include newline line endings :raises: KeyError if include_tables or exclude_tables includes a mis-specified table that is not included in filepath_a OR filepath_b """ def _create_unified_diff( left_ttxpath: Text, right_ttxpath: Text, pre_pathname: Text, prepath: Text, post_pathname: Text, postpath: Text, ) -> Iterable[Text]: with open(left_ttxpath) as ff: fromlines = ff.readlines() with open(right_ttxpath) as tf: tolines = tf.readlines() fromdate = get_file_modtime(prepath) todate = get_file_modtime(postpath) yield from unified_diff( fromlines, tolines, pre_pathname, post_pathname, fromdate, todate, n=context_lines, ) yield from _diff_with_saved_ttx_files( filepath_a, filepath_b, include_tables, exclude_tables, font_number_a, font_number_b, use_multiprocess, _create_unified_diff, ) def run_external_diff( diff_tool: Text, diff_args: List[Text], filepath_a: Text, filepath_b: Text, include_tables: Optional[List[Text]] = None, exclude_tables: Optional[List[Text]] = None, font_number_a: int = -1, font_number_b: int = -1, use_multiprocess: bool = True, ) -> Iterator[Text]: """Performs a unified diff on a TTX serialized data format dump of font binary data using an external diff executable that is requested by the caller via `command` diff_tool: (string) command line executable string diff_args: (list of strings) arguments for the diff tool filepath_a: (string) pre-file local file path filepath_b: (string) post-file local file path include_tables: (list of str) Python list of OpenType tables to include in the diff exclude_tables: (list of str) Python list of OpentType tables to exclude from the diff use_multiprocess: (bool) use multi-processor optimizations (default=True) include_tables and exclude_tables are mutually exclusive arguments. Only one should be defined :returns: Generator of ordered diff line strings that include newline line endings :raises: KeyError if include_tables or exclude_tables includes a mis-specified table that is not included in filepath_a OR filepath_b :raises: IOError if exception raised during execution of `command` on TTX files """ def _create_external_diff( left_ttxpath: Text, right_ttxpath: Text, _pre_pathname: Text, _prepath: Text, _post_pathname: Text, _postpath: Text, ) -> Iterable[Text]: command = [diff_tool] + diff_args + [left_ttxpath, right_ttxpath] process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf8", ) for line in process.stdout: yield line err = process.stderr.read() if err: raise IOError(err) yield from _diff_with_saved_ttx_files( filepath_a, filepath_b, include_tables, exclude_tables, font_number_a, font_number_b, use_multiprocess, _create_external_diff, )