diff options
| author | robot-piglet <[email protected]> | 2026-03-24 22:03:23 +0300 |
|---|---|---|
| committer | robot-piglet <[email protected]> | 2026-03-24 22:34:09 +0300 |
| commit | 6092233e61d1dc129fe1eb007399cc192c5ceb59 (patch) | |
| tree | 90522e5b7449e5cdb06bd24eafb333b9e9d3e9f1 /contrib/python/fonttools/fontTools/diff | |
| parent | c8c3fda4b2e47ceaad9790b7a5fb192110162f15 (diff) | |
Intermediate changes
commit_hash:5e2a2254279501ad2bde571fbd53c1a27a00e898
Diffstat (limited to 'contrib/python/fonttools/fontTools/diff')
| -rw-r--r-- | contrib/python/fonttools/fontTools/diff/__init__.py | 441 | ||||
| -rw-r--r-- | contrib/python/fonttools/fontTools/diff/__main__.py | 6 | ||||
| -rw-r--r-- | contrib/python/fonttools/fontTools/diff/color.py | 44 | ||||
| -rw-r--r-- | contrib/python/fonttools/fontTools/diff/diff.py | 294 | ||||
| -rw-r--r-- | contrib/python/fonttools/fontTools/diff/utils.py | 28 |
5 files changed, 813 insertions, 0 deletions
diff --git a/contrib/python/fonttools/fontTools/diff/__init__.py b/contrib/python/fonttools/fontTools/diff/__init__.py new file mode 100644 index 00000000000..b876520b2a6 --- /dev/null +++ b/contrib/python/fonttools/fontTools/diff/__init__.py @@ -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 diff --git a/contrib/python/fonttools/fontTools/diff/__main__.py b/contrib/python/fonttools/fontTools/diff/__main__.py new file mode 100644 index 00000000000..8dbef1dfc5d --- /dev/null +++ b/contrib/python/fonttools/fontTools/diff/__main__.py @@ -0,0 +1,6 @@ +import sys +from fontTools.diff import main + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/contrib/python/fonttools/fontTools/diff/color.py b/contrib/python/fonttools/fontTools/diff/color.py new file mode 100644 index 00000000000..e6ef489d67e --- /dev/null +++ b/contrib/python/fonttools/fontTools/diff/color.py @@ -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 diff --git a/contrib/python/fonttools/fontTools/diff/diff.py b/contrib/python/fonttools/fontTools/diff/diff.py new file mode 100644 index 00000000000..302f2e677ae --- /dev/null +++ b/contrib/python/fonttools/fontTools/diff/diff.py @@ -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, + ) diff --git a/contrib/python/fonttools/fontTools/diff/utils.py b/contrib/python/fonttools/fontTools/diff/utils.py new file mode 100644 index 00000000000..aed97063636 --- /dev/null +++ b/contrib/python/fonttools/fontTools/diff/utils.py @@ -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] |
