text: # Copyright 2018 Adobe. All rights reserved.
  
  """
  Helper script for diff'ing files.
  Used as part of the integration tests.
  """
  
  from __future__ import print_function, division, absolute_import
  
  import argparse
  import difflib
  import filecmp
  import logging
  import os
  import re
  import sys
  
  from fontTools.misc.py23 import open
  
  __version__ = '0.3.0'
  
  logger = logging.getLogger('differ')
  
  
  TXT_MODE = 'txt'
  BIN_MODE = 'bin'
  SPLIT_MARKER = '_+:+_'
  DFLT_ENC = 'utf-8'
  
  
  class Differ(object):
  
      def __init__(self, opts):
          self.mode = opts.mode
          self.path1 = opts.path1
          self.path2 = opts.path2
          # tuple of strings containing the starts of lines to skip
          self.skip_strings = opts.skip_strings
          # tuple of integers for the line numbers to skip
          self.skip_lines = opts.skip_lines
          # regex pattern matching the beginning of line
          self.skip_regex = opts.skip_regex
          self.encoding = opts.encoding
  
      def diff_paths(self):
          """
          Diffs the contents of two paths using the parameters provided.
          Returns True if the contents match, and False if they don't.
          """
          if self.mode == TXT_MODE:
              if os.path.isfile(self.path1):
                  return self._diff_txt_files(self.path1, self.path2)
              elif os.path.isdir(self.path1):
                  return self._diff_dirs()
          elif self.mode == BIN_MODE:
              if os.path.isfile(self.path1):
                  return filecmp.cmp(self.path1, self.path2)
              elif os.path.isdir(self.path1):
                  return self._diff_dirs()
  
      def _diff_txt_files(self, path1, path2):
          """
          Diffs two text-based files using difflib.
          Returns True if the contents match, and False if they don't.
  
          NOTE: This method CANNOT use self.path1 and self.path2 because it
          gets called from _diff_dirs().
          """
          expected = self._read_txt_file(path1)
          actual = self._read_txt_file(path2)
          if actual != expected:
              for line in difflib.unified_diff(expected, actual,
                                               fromfile=path1,
                                               tofile=path2, n=1):
                  sys.stdout.write(line)
              return False
          return True
  
      def _read_txt_file(self, path):
          """
          Reads a text file and returns a list of its lines.
          """
          # Hard code a first line; this way the difflib results start
          # from 1 instead of zero, thus matching the file's line numbers
          lines = ['']
          try:
              with open(path, "r", encoding=self.encoding) as f:
                  for i, line in enumerate(f.readlines(), 1):
                      # Skip lines that change, such as timestamps
                      if self._line_to_skip(line):
                          logger.debug("Matched begin of line. Skipped: {}"
                                       "".format(line.rstrip()))
                          # Blank the line instead of actually skipping (via
                          # 'continue'); this way the difflib results show the
                          # correct line numbers
                          line = ''
                      # Skip specific lines, referenced by number
                      elif i in self.skip_lines:
                          logger.debug("Matched line #{}. Skipped: {}"
                                       "".format(i, line.rstrip()))
                          line = ''
                      # Skip lines that match regex
                      elif self.skip_regex and self.skip_regex.match(line):
                          logger.debug("Matched regex begin of line. Skipped: {}"
                                       "".format(line.rstrip()))
                          line = ''
                      # Use os-native line separator to enable running difflib
                      lines.append(line.rstrip() + os.linesep)
          except UnicodeDecodeError:
              logger.error("Couldn't read text file using '{}' encoding.\n"
                           "      File path: {}".format(self.encoding, path))
              sys.exit(1)
          return lines
  
      def _line_to_skip(self, line):
          """
          Loops over the skip items.
          Returns True if the beginning of the line matches a skip item.
          """
          for item in self.skip_strings:
              if line.startswith(item):
                  return True
          return False
  
      def _diff_dirs(self):
          """
          Diffs two folders containing files.
          Returns True if all files match. Returns False if the folders' contents
          don't match, or as soon as one non-matching file is found.
          """
          all_rel_file_paths = self._compare_dir_contents()
          if all_rel_file_paths is None:
              return False
          for rel_file_path in all_rel_file_paths:
              path1 = self.path1 + rel_file_path
              assert os.path.exists(path1), "Not a valid path1: {}".format(path1)
              path2 = self.path2 + rel_file_path
              assert os.path.exists(path2), "Not a valid path2: {}".format(path2)
              if self.mode == BIN_MODE:
                  diff_result = filecmp.cmp(path1, path2)
              else:
                  diff_result = self._diff_txt_files(path1, path2)
              if not diff_result:
                  logger.debug("Non-matching file: {}".format(rel_file_path))
                  return False
          return True
  
      def _report_dir_diffs(self, all_paths1, all_paths2):
          """
          Returns a string listing the paths that exist in folder 1 but not in 2,
          and vice-versa.
          """
          diffs_str = ''
          set_1st = set(all_paths1)
          set_2nd = set(all_paths2)
          diff1 = sorted(list(set_1st - set_2nd))
          diff2 = sorted(list(set_2nd - set_1st))
          if diff1:
              dir1 = os.path.basename(self.path1)
              diffs_str += ("\n  In 1st folder ({}) but not in 2nd:\n    {}"
                            "".format(dir1, '\n    '.join(diff1)))
          if diff2:
              dir2 = os.path.basename(self.path2)
              diffs_str += ("\n  In 2nd folder ({}) but not in 1st:\n    {}"
                            "".format(dir2, '\n    '.join(diff2)))
          return diffs_str
  
      def _compare_dir_contents(self):
          """
          Checks if two directory trees have the same files and folders.
          Returns a list of relative paths to all files if the dirs' contents
          match, and None if they don't.
          """
          all_paths1 = self._get_all_file_paths_in_dir_tree(self.path1)
          all_paths2 = self._get_all_file_paths_in_dir_tree(self.path2)
          if all_paths1 != all_paths2:
              logger.info("Folders' contents don't match.{}".format(
                  self._report_dir_diffs(all_paths1, all_paths2)))
              return None
          return all_paths1
  
      @staticmethod
      def _get_all_file_paths_in_dir_tree(start_path):
          """
          Returns a list of relative paths of all files in a directory tree.
          The list's items are ordered top-down according to the tree.
          """
          all_paths = []
          for dir_name, _, file_names in os.walk(start_path):
              all_paths.extend(
                  [os.path.join(dir_name, f_name) for f_name in file_names])
  
          # Make the paths relative, and enforce order.
          all_paths = sorted(
              [path.replace(start_path, '') for path in all_paths])
  
          logger.debug("All paths: {}".format(all_paths))
  
          return all_paths
  
  
  def _get_path_kind(pth):
      """
      Returns a string describing the kind of path.
      String values are file, folder, invalid, and unknown.
      """
      if not os.path.exists(pth):
          return 'invalid'
      elif os.path.isfile(pth):
          return 'file'
      elif os.path.isdir(pth):
          return 'folder'
      return 'unknown'
  
  
  def _paths_are_same_kind(path1, path2):
      """
      Checks that both paths are either files or folders.
      Returns boolean.
      """
      if all([os.path.isfile(path) for path in (path1, path2)]):
          return True
      elif all([os.path.isdir(path) for path in (path1, path2)]):
          return True
      return False
  
  
  def _validate_path(path_str):
      valid_path = os.path.abspath(os.path.realpath(path_str))
      if not os.path.exists(valid_path):
          raise argparse.ArgumentTypeError(
              "{} is not a valid path.".format(path_str))
      return valid_path
  
  
  def _split_string_sequence(str_seq):
      return tuple(str_seq.split(SPLIT_MARKER))
  
  
  def _split_num_range_or_delta(num_str):
      num_range = num_str.split('-') + ['-']
      num_delta = num_str.split('+') + ['+']
      if len(num_range) == 3:
          return num_range
      elif len(num_delta) == 3:
          return num_delta
      return num_str
  
  
  def _convert_to_int(num_str):
      try:
          return int(num_str)
      except ValueError:
          raise argparse.ArgumentTypeError("Not a number: {}".format(num_str))
  
  
  def _expand_num_range_or_delta(num_str_lst):
      start_num = _convert_to_int(num_str_lst[0])
      rng_dlt_num = _convert_to_int(num_str_lst[1])
      plus_minus = num_str_lst[2]
      if plus_minus == '+':
          return list(range(start_num, start_num + rng_dlt_num + 1))
      elif plus_minus == '-':
          if not (rng_dlt_num >= start_num):
              raise argparse.ArgumentTypeError(
                  "The start of range value is larger than the end of range "
                  "value: {}-{}".format(start_num, rng_dlt_num))
          return list(range(start_num, rng_dlt_num + 1))
      else:
          raise argparse.ArgumentTypeError(
              "Unrecognized number separator: {}".format(plus_minus))
  
  
  def _convert_seq_to_ints(num_seq):
      seq = []
      for item in num_seq:
          if isinstance(item, list):
              seq.extend(_expand_num_range_or_delta(item))
          else:
              seq.append(_convert_to_int(item))
      return seq
  
  
  def _split_linenumber_sequence(str_seq):
      num_seq = [_split_num_range_or_delta(item) for item in str_seq.split(',')]
      return tuple(_convert_seq_to_ints(num_seq))
  
  
  def _compile_regex(str_seq):
      if str_seq[0] != '^':
          raise argparse.ArgumentTypeError(
              "The expression must start with the caret '^' character")
      try:
          return re.compile(str_seq)
      except re.error as err:
          raise argparse.ArgumentTypeError(
              'The expression is invalid: {}'.format(err))
  
  
  def get_options(args):
      parser = argparse.ArgumentParser(
          formatter_class=argparse.RawTextHelpFormatter,
          description=__doc__
      )
      parser.add_argument(
          '--version',
          action='version',
          version=__version__
      )
      parser.add_argument(
          '-v',
          '--verbose',
          action='count',
          default=0,
          help='verbose mode\n'
               'Use -vv for debug mode'
      )
      parser.add_argument(
          '-m',
          '--mode',
          default=TXT_MODE,
          choices=(TXT_MODE, BIN_MODE),
          help='diff mode (default: %(default)s)'
      )
      parser.add_argument(
          '-s',
          '--string',
          dest='skip_strings',
          type=_split_string_sequence,
          default=(),
          help='string for matching the beginning of a line to skip\n'
               'For multiple strings, separate them with {}'.format(SPLIT_MARKER)
      )
      parser.add_argument(
          '-l',
          '--line',
          dest='skip_lines',
          type=_split_linenumber_sequence,
          default=(),
          help='number of a line to skip\n'
               'For multiple line numbers, separate them with a comma (,).\n'
               'For ranges of lines, use a minus (-) between two numbers.\n'
               'For a line delta, use a plus (+) between two numbers.'
      )
      parser.add_argument(
          '-r',
          '--regex',
          dest='skip_regex',
          type=_compile_regex,
          help='regular expression matching the beginning of a line to skip\n'
               "The expression must start with the caret '^' character, "
               'and characters such as backslash, semicolon, and space need '
               'to be escaped.'
      )
      parser.add_argument(
          '-e',
          '--encoding',
          default=DFLT_ENC,
          choices=(DFLT_ENC, 'macroman'),
          help='encoding to use when opening text files (default: %(default)s)'
      )
      parser.add_argument(
          'path1',
          metavar='PATH1',
          type=_validate_path,
          help='1st path for comparison'
      )
      parser.add_argument(
          'path2',
          metavar='PATH2',
          type=_validate_path,
          help='2nd path for comparison'
      )
      options = parser.parse_args(args)
  
      if not options.verbose:
          level = "WARNING"
      elif options.verbose == 1:
          level = "INFO"
      else:
          level = "DEBUG"
      logging.basicConfig(level=level)
  
      if not _paths_are_same_kind(options.path1, options.path2):
          parser.error("The paths are not of the same kind. Path1's kind is {}. "
                       "Path2's kind is {}.".format(
                           _get_path_kind(options.path1),
                           _get_path_kind(options.path2)))
  
      logger.debug("Line numbers: {}".format(options.skip_lines))
      logger.debug("Regular expression: {}".format(
          getattr(options.skip_regex, 'pattern', None)))
  
      return options
  
  
  def main(args=None):
      """
      Returns True if the inputs match, and False if they don't.
      """
      opts = get_options(args)
  
      differ = Differ(opts)
      return differ.diff_paths()
  
  
  if __name__ == "__main__":
      sys.exit(0 if main() else 1)
  