Skip to content

Commit f3ca580

Browse files
committed
[fdiff.diff] add support for table inclusion and exclusion filters in unified diffs
1 parent 3fe0720 commit f3ca580

File tree

2 files changed

+131
-3
lines changed

2 files changed

+131
-3
lines changed

lib/fdiff/diff.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,66 @@
77
from fdiff.thirdparty.fdifflib import unified_diff
88

99

10-
def u_diff(filepath_a, filepath_b, context_lines=3):
10+
def u_diff(
11+
filepath_a, filepath_b, context_lines=3, include_tables=None, exclude_tables=None
12+
):
13+
"""Performs a unified diff on a TTX serialized data format dump of font binary data using
14+
a modified version of the Python standard libary difflib module.
15+
16+
filepath_a: (string) pre-file path
17+
filepath_b: (string) post-file path
18+
context_lines: (int) number of context lines to include in the diff (default=3)
19+
include_tables: (list of str) Python list of OpenType tables to include in the diff
20+
exclude_tables: (list of str) Python list of OpentType tables to exclude from the diff
21+
22+
include_tables and exclude_tables are mutually exclusive arguments. Only one should
23+
be defined
24+
25+
:returns: Generator of ordered diff line strings that include newline line endings
26+
:raises: KeyError if include_tables or exclude_tables includes a mis-specified table
27+
that is not included in filepath_a OR filepath_b"""
1128
tt_left = TTFont(filepath_a)
1229
tt_right = TTFont(filepath_b)
1330

31+
# Validation: include_tables request should be for tables that are in one of
32+
# the two fonts. This otherwise silently passes with exit status code 0 which
33+
# could lead to the interpretation of no diff between two files when the table
34+
# entry is incorrectly defined or is a typo. Let's be conservative and consider
35+
# this an error, force user to use explicit definitions that include tables in
36+
# one of the two files, and understand that the diff request was for one or more
37+
# tables that are not present.
38+
if include_tables is not None:
39+
for table in include_tables:
40+
if table not in tt_left and table not in tt_right:
41+
raise KeyError(
42+
f"'{table}' table was not identified for inclusion in either font"
43+
)
44+
45+
# Validation: exclude_tables request should be for tables that are in one of
46+
# the two fonts. Mis-specified OT table definitions could otherwise result
47+
# in the presence of a table in the diff when the request was to exclude it.
48+
# For example, when an "OS/2" table request is entered as "OS2".
49+
if exclude_tables is not None:
50+
for table in exclude_tables:
51+
if table not in tt_left and table not in tt_right:
52+
raise KeyError(
53+
f"'{table}' table was not identified for exclusion in either font"
54+
)
55+
1456
fromdate = get_file_modtime(filepath_a)
1557
todate = get_file_modtime(filepath_b)
1658

1759
with tempfile.TemporaryDirectory() as tmpdirname:
18-
tt_left.saveXML(os.path.join(tmpdirname, "left.ttx"))
19-
tt_right.saveXML(os.path.join(tmpdirname, "right.ttx"))
60+
tt_left.saveXML(
61+
os.path.join(tmpdirname, "left.ttx"),
62+
tables=include_tables,
63+
skipTables=exclude_tables,
64+
)
65+
tt_right.saveXML(
66+
os.path.join(tmpdirname, "right.ttx"),
67+
tables=include_tables,
68+
skipTables=exclude_tables,
69+
)
2070

2171
with open(os.path.join(tmpdirname, "left.ttx")) as ff:
2272
fromlines = ff.readlines()

tests/test_diff.py

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
ROBOTO_UDIFF_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_expected.txt")
1010
ROBOTO_UDIFF_COLOR_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_color_expected.txt")
1111
ROBOTO_UDIFF_1CONTEXT_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_1context_expected.txt")
12+
ROBOTO_UDIFF_HEADONLY_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_headonly_expected.txt")
13+
ROBOTO_UDIFF_HEADPOSTONLY_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_headpostonly_expected.txt")
14+
ROBOTO_UDIFF_EXCLUDE_HEADPOST_EXPECTED_PATH = os.path.join("tests", "testfiles", "roboto_udiff_ex_headpost_expected.txt")
1215

1316
# Setup: define the expected diff text for unified diff
1417
with open(ROBOTO_UDIFF_EXPECTED_PATH, "r") as robo_udiff:
@@ -22,6 +25,18 @@
2225
with open(ROBOTO_UDIFF_1CONTEXT_EXPECTED_PATH, "r") as robo_udiff_contextlines:
2326
ROBOTO_UDIFF_1CONTEXT_EXPECTED = robo_udiff_contextlines.read()
2427

28+
# Setup: define the expected diff text for head table only diff
29+
with open(ROBOTO_UDIFF_HEADONLY_EXPECTED_PATH, "r") as robo_udiff_headonly:
30+
ROBOTO_UDIFF_HEADONLY_EXPECTED = robo_udiff_headonly.read()
31+
32+
# Setup: define the expected diff text for head and post table only diff
33+
with open(ROBOTO_UDIFF_HEADPOSTONLY_EXPECTED_PATH, "r") as robo_udiff_headpostonly:
34+
ROBOTO_UDIFF_HEADPOSTONLY_EXPECTED = robo_udiff_headpostonly.read()
35+
36+
# Setup: define the expected diff text for head and post table only diff
37+
with open(ROBOTO_UDIFF_EXCLUDE_HEADPOST_EXPECTED_PATH, "r") as robo_udiff_ex_headpost:
38+
ROBOTO_UDIFF_EXCLUDE_HEADPOST_EXPECTED = robo_udiff_ex_headpost.read()
39+
2540

2641
def test_unified_diff_default():
2742
res = u_diff(ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH)
@@ -61,5 +76,68 @@ def test_unified_diff_context_lines_1():
6176
assert line == expected_string_list[x]
6277

6378

79+
def test_unified_diff_head_table_only():
80+
res = u_diff(ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH, include_tables=["head"])
81+
res_string = "".join(res)
82+
res_string_list = res_string.split("\n")
83+
expected_string_list = ROBOTO_UDIFF_HEADONLY_EXPECTED.split("\n")
84+
85+
# have to handle the tests for the top two file path lines
86+
# differently than the rest of the comparisons because
87+
# the time is defined using local platform settings
88+
# which makes tests fail on remote CI testing services vs.
89+
# my local testing platform...
90+
for x, line in enumerate(res_string_list):
91+
# treat top two lines of the diff as comparison of first 10 chars only
92+
if x in (0, 1):
93+
assert line[0:9] == expected_string_list[x][0:9]
94+
else:
95+
assert line == expected_string_list[x]
96+
97+
98+
def test_unified_diff_headpost_table_only():
99+
res = u_diff(ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH, include_tables=["head", "post"])
100+
res_string = "".join(res)
101+
res_string_list = res_string.split("\n")
102+
expected_string_list = ROBOTO_UDIFF_HEADPOSTONLY_EXPECTED.split("\n")
103+
104+
# have to handle the tests for the top two file path lines
105+
# differently than the rest of the comparisons because
106+
# the time is defined using local platform settings
107+
# which makes tests fail on remote CI testing services vs.
108+
# my local testing platform...
109+
for x, line in enumerate(res_string_list):
110+
# treat top two lines of the diff as comparison of first 10 chars only
111+
if x in (0, 1):
112+
assert line[0:9] == expected_string_list[x][0:9]
113+
else:
114+
assert line == expected_string_list[x]
115+
116+
117+
def test_unified_diff_exclude_headpost_tables():
118+
res = u_diff(ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH, exclude_tables=["head", "post"])
119+
res_string = "".join(res)
120+
res_string_list = res_string.split("\n")
121+
expected_string_list = ROBOTO_UDIFF_EXCLUDE_HEADPOST_EXPECTED.split("\n")
122+
123+
# have to handle the tests for the top two file path lines
124+
# differently than the rest of the comparisons because
125+
# the time is defined using local platform settings
126+
# which makes tests fail on remote CI testing services vs.
127+
# my local testing platform...
128+
for x, line in enumerate(res_string_list):
129+
# treat top two lines of the diff as comparison of first 10 chars only
130+
if x in (0, 1):
131+
assert line[0:9] == expected_string_list[x][0:9]
132+
else:
133+
assert line == expected_string_list[x]
134+
135+
136+
def test_unified_diff_include_with_bad_table_definition():
137+
with pytest.raises(KeyError):
138+
u_diff(ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH, include_tables=["bogus"])
64139

65140

141+
def test_unified_diff_exclude_with_bad_table_definition():
142+
with pytest.raises(KeyError):
143+
u_diff(ROBOTO_BEFORE_PATH, ROBOTO_AFTER_PATH, exclude_tables=["bogus"])

0 commit comments

Comments
 (0)