| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- 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,
- )
|