295 lines
8.8 KiB
Python
295 lines
8.8 KiB
Python
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,
|
|
)
|