hand
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
from typing import Iterable, Iterator, List, Optional, Text, Tuple
|
||||
|
||||
from .color import color_unified_diff_line
|
||||
from .diff import run_external_diff, u_diff
|
||||
from .utils import file_exists, get_tables_argument_list
|
||||
|
||||
|
||||
def pipe_output(output: str) -> None:
|
||||
"""Pipes output to a pager if stdout is a TTY and a pager is available."""
|
||||
|
||||
if not output:
|
||||
return
|
||||
|
||||
if not sys.stdout.isatty():
|
||||
sys.stdout.write(output)
|
||||
return
|
||||
|
||||
pager = os.getenv("PAGER") or shutil.which("less")
|
||||
|
||||
if not pager:
|
||||
sys.stdout.write(output)
|
||||
return
|
||||
|
||||
pager_cmd = [pager]
|
||||
if "less" in os.path.basename(pager):
|
||||
pager_cmd.append("-R")
|
||||
|
||||
proc = subprocess.Popen(pager_cmd, stdin=subprocess.PIPE, text=True)
|
||||
try:
|
||||
proc.stdin.write(output)
|
||||
proc.stdin.close()
|
||||
proc.wait()
|
||||
except (BrokenPipeError, KeyboardInterrupt):
|
||||
# Pager process was terminated before all output was written.
|
||||
# This is not an error. The main exception handler will deal with it.
|
||||
if proc.stdin:
|
||||
proc.stdin.close()
|
||||
# The process might still be running, but we have closed our side of the
|
||||
# pipe. The Popen destructor will send a SIGKILL to the child.
|
||||
except Exception:
|
||||
if proc.stdin:
|
||||
proc.stdin.close()
|
||||
raise
|
||||
|
||||
|
||||
def _is_gnu_diff(diff_tool: str) -> bool:
|
||||
"""Returns True if the provided diff executable is GNU diff."""
|
||||
try:
|
||||
proc = subprocess.run(
|
||||
[diff_tool, "--version"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
)
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
version_output = (proc.stdout or "") + (proc.stderr or "")
|
||||
return "GNU diffutils" in version_output
|
||||
|
||||
|
||||
def _iter_filtered_table_tags(
|
||||
tags: Iterable[str],
|
||||
include_tables: Optional[List[str]] = None,
|
||||
exclude_tables: Optional[List[str]] = None,
|
||||
) -> Iterator[str]:
|
||||
for tag in tags:
|
||||
if exclude_tables and tag in exclude_tables:
|
||||
continue
|
||||
if include_tables and tag not in include_tables:
|
||||
continue
|
||||
yield tag
|
||||
|
||||
|
||||
def summarize(
|
||||
file1: str,
|
||||
file2: str,
|
||||
include_tables: Optional[List[str]] = None,
|
||||
exclude_tables: Optional[List[str]] = None,
|
||||
font_number_1: int = -1,
|
||||
font_number_2: int = -1,
|
||||
) -> Tuple[bool, str]:
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
with (
|
||||
TTFont(file1, lazy=True, fontNumber=font_number_1) as font1,
|
||||
TTFont(file2, lazy=True, fontNumber=font_number_2) as font2,
|
||||
):
|
||||
tags1 = {str(tag) for tag in font1.reader.keys()}
|
||||
tags2 = {str(tag) for tag in font2.reader.keys()}
|
||||
|
||||
all_tags = sorted(
|
||||
set(
|
||||
_iter_filtered_table_tags(
|
||||
tags1 | tags2,
|
||||
include_tables=include_tables,
|
||||
exclude_tables=exclude_tables,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
only1 = [tag for tag in all_tags if tag in tags1 and tag not in tags2]
|
||||
only2 = [tag for tag in all_tags if tag in tags2 and tag not in tags1]
|
||||
both = [tag for tag in all_tags if tag in tags1 and tag in tags2]
|
||||
|
||||
identical = True
|
||||
lines: List[str] = []
|
||||
|
||||
lines.append(f"Binary table summary:\n")
|
||||
lines.append(f" file1: {file1}\n")
|
||||
lines.append(f" file2: {file2}\n")
|
||||
|
||||
if only1:
|
||||
identical = False
|
||||
lines.append(f"\nTables only in file1 ({len(only1)}):\n")
|
||||
for tag in only1:
|
||||
lines.append(f"- {tag} ({len(font1.reader[tag])} bytes)\n")
|
||||
if only2:
|
||||
identical = False
|
||||
lines.append(f"\nTables only in file2 ({len(only2)}):\n")
|
||||
for tag in only2:
|
||||
lines.append(f"+ {tag} ({len(font2.reader[tag])} bytes)\n")
|
||||
|
||||
lines.append(f"\nTables in both ({len(both)}):\n")
|
||||
for tag in both:
|
||||
data1 = font1.reader[tag]
|
||||
data2 = font2.reader[tag]
|
||||
if data1 == data2:
|
||||
lines.append(f" {tag}: SAME ({len(data1)} bytes)\n")
|
||||
else:
|
||||
identical = False
|
||||
lines.append(f"* {tag}: DIFF ({len(data1)} vs {len(data2)} bytes)\n")
|
||||
|
||||
if identical:
|
||||
lines.append("\nResult: SAME\n")
|
||||
else:
|
||||
lines.append("\nResult: DIFFERENT\n")
|
||||
|
||||
return identical, "".join(lines)
|
||||
|
||||
|
||||
def get_binary_exclude_tables(
|
||||
file1: str,
|
||||
file2: str,
|
||||
include_tables: Optional[List[str]] = None,
|
||||
exclude_tables: Optional[List[str]] = None,
|
||||
font_number_1: int = -1,
|
||||
font_number_2: int = -1,
|
||||
) -> Tuple[bool, str]:
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
with (
|
||||
TTFont(file1, lazy=True, fontNumber=font_number_1) as font1,
|
||||
TTFont(file2, lazy=True, fontNumber=font_number_2) as font2,
|
||||
):
|
||||
tags1 = {str(tag) for tag in font1.reader.keys()}
|
||||
tags2 = {str(tag) for tag in font2.reader.keys()}
|
||||
|
||||
all_tags = sorted(
|
||||
set(
|
||||
_iter_filtered_table_tags(
|
||||
tags1 | tags2,
|
||||
include_tables=include_tables,
|
||||
exclude_tables=exclude_tables,
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
both = [tag for tag in all_tags if tag in tags1 and tag in tags2]
|
||||
out = set()
|
||||
|
||||
for tag in both:
|
||||
data1 = font1.reader[tag]
|
||||
data2 = font2.reader[tag]
|
||||
if data1 == data2:
|
||||
out.add(tag)
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
"""Compare two fonts for differences"""
|
||||
# try/except block rationale:
|
||||
# handles "premature" socket closure exception that is
|
||||
# raised by Python when stdout is piped to tools like
|
||||
# the `head` executable and socket is closed early
|
||||
# see: https://docs.python.org/3/library/signal.html#note-on-sigpipe
|
||||
ret = 0
|
||||
try:
|
||||
ret = run(sys.argv[1:])
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except BrokenPipeError:
|
||||
# Python flushes standard streams on exit; redirect remaining output
|
||||
# to devnull to avoid another BrokenPipeError at shutdown
|
||||
devnull = os.open(os.devnull, os.O_WRONLY)
|
||||
os.dup2(devnull, sys.stdout.fileno())
|
||||
return ret
|
||||
|
||||
|
||||
def run(argv: List[Text]):
|
||||
# ------------------------------------------
|
||||
# argparse command line argument definitions
|
||||
# ------------------------------------------
|
||||
parser = argparse.ArgumentParser(
|
||||
description="An OpenType table diff tool for fonts."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-l",
|
||||
"--summary",
|
||||
action="store_true",
|
||||
help="Report table presence and binary equality only",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-U",
|
||||
"--lines",
|
||||
type=int,
|
||||
default=3,
|
||||
help="Number of context lines for unified diff (default: 3)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-t",
|
||||
"--include",
|
||||
type=str,
|
||||
nargs="+",
|
||||
default=None,
|
||||
help="Font tables to include. Multiple options are allowed.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-x",
|
||||
"--exclude",
|
||||
type=str,
|
||||
nargs="+",
|
||||
default=None,
|
||||
help="Font tables to exclude. Multiple options are allowed.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diff", type=str, help="Run external diff tool command (default: diff)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--diff-arg",
|
||||
type=str,
|
||||
default=None,
|
||||
help="External diff tool arguments (default: -u)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--color",
|
||||
choices=["auto", "never", "always"],
|
||||
default="auto",
|
||||
help="Whether to colorize output (default: auto)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--y1",
|
||||
type=int,
|
||||
default=-1,
|
||||
metavar="NUMBER",
|
||||
help="Select font number for TrueType Collection (.ttc/.otc) FILE1, starting from 0",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--y2",
|
||||
type=int,
|
||||
default=-1,
|
||||
metavar="NUMBER",
|
||||
help="Select font number for TrueType Collection (.ttc/.otc) FILE2, starting from 0",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-a",
|
||||
"--always",
|
||||
action="store_true",
|
||||
help="Compare tables even if binary identical",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-b",
|
||||
"--binary",
|
||||
action="store_true",
|
||||
help="Compare tables only if binaries differ (default)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-q", "--quiet", action="store_true", help="Suppress all output"
|
||||
)
|
||||
parser.add_argument("FILE1", help="Font file path 1")
|
||||
parser.add_argument("FILE2", help="Font file path 2")
|
||||
|
||||
args: argparse.Namespace = parser.parse_args(argv)
|
||||
|
||||
# /////////////////////////////////////////////////////////
|
||||
#
|
||||
# Validations
|
||||
#
|
||||
# /////////////////////////////////////////////////////////
|
||||
|
||||
# ----------------------------------
|
||||
# Incompatible argument validations
|
||||
# ----------------------------------
|
||||
|
||||
if args.always and args.binary:
|
||||
if not args.quiet:
|
||||
sys.stderr.write(
|
||||
f"[*] Error: --always and --binary are mutually exclusive options. "
|
||||
f"Please use ONLY one of these options in your command.{os.linesep}"
|
||||
)
|
||||
return 2
|
||||
if not args.always:
|
||||
args.binary = True
|
||||
|
||||
# -------------------------------
|
||||
# File path argument validations
|
||||
# -------------------------------
|
||||
|
||||
if not file_exists(args.FILE1):
|
||||
if not args.quiet:
|
||||
sys.stderr.write(
|
||||
f"[*] ERROR: The file path '{args.FILE1}' can not be found.{os.linesep}"
|
||||
)
|
||||
return 2
|
||||
if not file_exists(args.FILE2):
|
||||
if not args.quiet:
|
||||
sys.stderr.write(
|
||||
f"[*] ERROR: The file path '{args.FILE2}' can not be found.{os.linesep}"
|
||||
)
|
||||
return 2
|
||||
|
||||
# /////////////////////////////////////////////////////////
|
||||
#
|
||||
# Command line logic
|
||||
#
|
||||
# /////////////////////////////////////////////////////////
|
||||
|
||||
# parse explicitly included or excluded tables in
|
||||
# the command line arguments
|
||||
# set as a Python list if it was defined on the command line
|
||||
# or as None if it was not set on the command line
|
||||
include_list: Optional[List[Text]] = get_tables_argument_list(args.include)
|
||||
exclude_list: Optional[List[Text]] = get_tables_argument_list(args.exclude)
|
||||
|
||||
if args.summary:
|
||||
try:
|
||||
identical, output = summarize(
|
||||
args.FILE1,
|
||||
args.FILE2,
|
||||
include_tables=include_list,
|
||||
exclude_tables=exclude_list,
|
||||
font_number_1=args.y1,
|
||||
font_number_2=args.y2,
|
||||
)
|
||||
if not args.quiet:
|
||||
sys.stdout.write(output)
|
||||
return 0 if identical else 1
|
||||
except Exception as e:
|
||||
if not args.quiet:
|
||||
sys.stderr.write(f"[*] ERROR: {e}{os.linesep}")
|
||||
return 2
|
||||
|
||||
if args.binary:
|
||||
excluded_binary_tables = get_binary_exclude_tables(
|
||||
args.FILE1,
|
||||
args.FILE2,
|
||||
include_tables=include_list,
|
||||
exclude_tables=exclude_list,
|
||||
font_number_1=args.y1,
|
||||
font_number_2=args.y2,
|
||||
)
|
||||
if include_list is not None:
|
||||
include_list = [
|
||||
tag for tag in include_list if tag not in excluded_binary_tables
|
||||
]
|
||||
else:
|
||||
if exclude_list is None:
|
||||
exclude_list = []
|
||||
exclude_list.extend(sorted(excluded_binary_tables))
|
||||
|
||||
diff_tool = args.diff
|
||||
color_output = args.color == "always" or (
|
||||
args.color == "auto" and sys.stdout.isatty
|
||||
)
|
||||
|
||||
if diff_tool is None:
|
||||
diff_tool = shutil.which("diff")
|
||||
elif diff_tool:
|
||||
diff_tool = shutil.which(diff_tool)
|
||||
if diff_tool is None:
|
||||
if not args.quiet:
|
||||
sys.stderr.write(
|
||||
f"[*] ERROR: The external diff tool executable "
|
||||
f"'{args.diff}' was not found.{os.linesep}"
|
||||
)
|
||||
return 2
|
||||
|
||||
try:
|
||||
if diff_tool:
|
||||
diff_arg = args.diff_arg
|
||||
if diff_arg is None:
|
||||
if args.lines == 3:
|
||||
diff_arg = ["-u"]
|
||||
else:
|
||||
diff_arg = ["-u{}".format(args.lines)]
|
||||
if _is_gnu_diff(diff_tool):
|
||||
diff_arg.append(r"-F^\s\s<")
|
||||
else:
|
||||
diff_arg = diff_arg.split()
|
||||
|
||||
output = run_external_diff(
|
||||
diff_tool,
|
||||
diff_arg,
|
||||
args.FILE1,
|
||||
args.FILE2,
|
||||
include_tables=include_list,
|
||||
exclude_tables=exclude_list,
|
||||
font_number_a=args.y1,
|
||||
font_number_b=args.y2,
|
||||
use_multiprocess=True,
|
||||
)
|
||||
else:
|
||||
output = u_diff(
|
||||
args.FILE1,
|
||||
args.FILE2,
|
||||
context_lines=args.lines,
|
||||
include_tables=include_list,
|
||||
exclude_tables=exclude_list,
|
||||
font_number_a=args.y1,
|
||||
font_number_b=args.y2,
|
||||
use_multiprocess=True,
|
||||
)
|
||||
|
||||
if color_output:
|
||||
output = [color_unified_diff_line(line) for line in output]
|
||||
|
||||
output = "".join(output)
|
||||
if not args.quiet:
|
||||
pipe_output(output)
|
||||
return 1 if output else 0
|
||||
|
||||
except Exception as e:
|
||||
if not args.quiet:
|
||||
sys.stderr.write(f"[*] ERROR: {e}{os.linesep}")
|
||||
return 2
|
||||
@@ -0,0 +1,6 @@
|
||||
import sys
|
||||
from fontTools.diff import main
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,44 @@
|
||||
from typing import Dict, Text
|
||||
|
||||
ansicolors: Dict[Text, Text] = {
|
||||
"BLACK": "\033[30m",
|
||||
"RED": "\033[31m",
|
||||
"GREEN": "\033[32m",
|
||||
"YELLOW": "\033[33m",
|
||||
"BLUE": "\033[34m",
|
||||
"MAGENTA": "\033[35m",
|
||||
"CYAN": "\033[36m",
|
||||
"WHITE": "\033[37m",
|
||||
"BOLD": "\033[1m",
|
||||
"RESET": "\033[0m",
|
||||
}
|
||||
|
||||
green_start: Text = ansicolors["GREEN"]
|
||||
red_start: Text = ansicolors["RED"]
|
||||
cyan_start: Text = ansicolors["CYAN"]
|
||||
reset: Text = ansicolors["RESET"]
|
||||
|
||||
|
||||
def color_unified_diff_line(line: Text) -> Text:
|
||||
"""Returns an ANSI escape code colored string with color based
|
||||
on the unified diff line type."""
|
||||
if line[0:2] == "+ ":
|
||||
return f"{green_start}{line}{reset}"
|
||||
elif line == "+\n":
|
||||
# some lines are formatted as hyphen only with no other characters
|
||||
# this indicates an added empty line
|
||||
return f"{green_start}{line}{reset}"
|
||||
elif line[0:2] == "- ":
|
||||
return f"{red_start}{line}{reset}"
|
||||
elif line == "-\n":
|
||||
# some lines are formatted as hyphen only with no other characters
|
||||
# this indicates a deleted empty line
|
||||
return f"{red_start}{line}{reset}"
|
||||
elif line[0:3] == "@@ ":
|
||||
return f"{cyan_start}{line}{reset}"
|
||||
elif line[0:4] == "--- ":
|
||||
return f"{red_start}{line}{reset}"
|
||||
elif line[0:4] == "+++ ":
|
||||
return f"{green_start}{line}{reset}"
|
||||
else:
|
||||
return line
|
||||
@@ -0,0 +1,294 @@
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional, Text, Union
|
||||
|
||||
|
||||
def file_exists(path: Union[bytes, str, "os.PathLike[Text]"]) -> bool:
|
||||
"""Validates file path as existing local file"""
|
||||
return os.path.isfile(path)
|
||||
|
||||
|
||||
def get_file_modtime(path: Union[bytes, str, "os.PathLike[Text]"]) -> Text:
|
||||
"""Returns ISO formatted file modification time in local system timezone"""
|
||||
return (
|
||||
datetime.fromtimestamp(os.stat(path).st_mtime, timezone.utc)
|
||||
.astimezone()
|
||||
.isoformat()
|
||||
)
|
||||
|
||||
|
||||
def get_tables_argument_list(table_list: Optional[List[Text]]) -> Optional[List[Text]]:
|
||||
"""Converts a list of OpenType table string into a Python list or
|
||||
return None if the table_list was not defined (i.e., it was not included
|
||||
in an option on the command line). Tables that are composed of three
|
||||
characters must be right padded with a space."""
|
||||
if table_list is None:
|
||||
return None
|
||||
else:
|
||||
return [table.ljust(4) for table in table_list]
|
||||
Reference in New Issue
Block a user