text: # -*- coding: utf-8 -*-
  # Copyright 2015-2016 Yelp
  #
  # Licensed under the Apache License, Version 2.0 (the "License");
  # you may not use this file except in compliance with the License.
  # You may obtain a copy of the License at
  #
  # http://www.apache.org/licenses/LICENSE-2.0
  #
  # Unless required by applicable law or agreed to in writing, software
  # distributed under the License is distributed on an "AS IS" BASIS,
  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  # See the License for the specific language governing permissions and
  # limitations under the License.
  """Merging errors, picking the best one, and displaying it."""
  import json
  
  from .ids import _time_sort_key
  
  
  def _pick_error(log_interpretation):
      """Pick most recent error from a dictionary possibly containing
      step, history, and task interpretations. Returns None if there
      are no errors.
      """
      def yield_errors():
          for log_type in ('step', 'history', 'task'):
              errors = log_interpretation.get(log_type, {}).get('errors')
              for error in errors or ():
                  yield error
  
      errors = _merge_and_sort_errors(yield_errors())
      if errors:
          return errors[0]
      else:
          return None
  
  
  def _merge_and_sort_errors(errors):
      """Merge errors from one or more lists of errors and then return
      them, sorted by recency.
  
      We allow None in place of an error list.
      """
      key_to_error = {}
  
      for error in errors:
          key = _time_sort_key(error)
          key_to_error.setdefault(key, {})
          key_to_error[key].update(error)
  
      # wrap sort key to prioritize task errors. See #1429
      def sort_key(key_and_error):
          key, error = key_and_error
  
          # key[0] is step number
          return (key[0], bool(error.get('task_error')), key[1:])
  
      return [error for key, error in
              sorted(key_to_error.items(), key=sort_key, reverse=True)]
  
  
  def _format_error(error):
      """Return string to log/print explaining the given error."""
      # it's just sad if we error while trying to explain an error
      try:
          return _format_error_helper(error)
      except:
          return json.dumps(error, indent=2, sort_keys=True)
  
  
  def _format_error_helper(error):
      """Return string to log/print explaining the given error."""
      result = ''
  
      hadoop_error = error.get('hadoop_error')
      if hadoop_error:
          result += hadoop_error.get('message', '')
  
          if hadoop_error.get('path'):
              result += '\n\n(from %s)' % _describe_source(hadoop_error)
  
      # for practical purposes, there's always a hadoop error with a message,
      # so don't worry too much about spacing.
  
      task_error = error.get('task_error')
      if task_error:
          if hadoop_error:
              result += '\n\ncaused by:\n\n%s' % (task_error.get('message', ''))
          else:
              result += task_error.get('message', '')
  
          if task_error.get('path'):
              result += '\n\n(from %s)' % _describe_source(task_error)
  
      split = error.get('split')
      if split and split.get('path'):
          result += '\n\nwhile reading input from %s' % _describe_source(split)
  
      return result
  
  
  def _describe_source(d):
      """return either '<path>' or 'line N of <path>' or 'lines M-N of <path>'.
      """
      path = d.get('path') or ''
  
      if 'num_lines' in d and 'start_line' in d:
          if d['num_lines'] == 1:
              return 'line %d of %s' % (d['start_line'] + 1, path)
          else:
              return 'lines %d-%d of %s' % (
                  d['start_line'] + 1, d['start_line'] + d['num_lines'], path)
      else:
          return path
  