docs: def pretty_table(rows, header=None):
      """
      Returns a string with a simple pretty table representing the given rows.
      Rows can be:
       - Sequences such as lists or tuples
       - Mappings such as dicts
       - Any object with a __dict__ attribute (most plain python objects) which is
         equivalent to passing the __dict__ directly.
      If no header is given then either all or none of the rows must be sequences
      to ensure the correct order. If there are no sequences then the header will be
      derived from the keys of the mappings.
  
      >>> print(pretty_table([['a', 'hello', 'c', 1], ['world', 'b', 'd', 2]]))
      a     | hello | c | 1
      world | b     | d | 2
      >>> print(pretty_table([['a', 'hello', 'c', 1], ['world', 'b', 'd', 2]], header='col1 col2 col3 col4'))
      col1  | col2  | col3 | col4
      ---------------------------
      a     | hello | c    | 1
      world | b     | d    | 2
      >>> print(pretty_table([{'a': 1, 'b': 2}, {'a': 3, 'b': 4}]))
      a | b
      -----
      1 | 2
      3 | 4
      >>> class C(object):
      ...     def __init__(self, a, b):
      ...         self.a = a
      ...         self.b = b
      ...
      >>> print(pretty_table([{'a': 1, 'b': 2}, C(3, 4), [5, 6]], header=['b', 'a']))
      b | a
      -----
      2 | 1
      4 | 3
      5 | 6
      >>> print(pretty_table([{'a': 1, 'b': 2}, C(3, 4), [5, 6]]))
      Traceback (most recent call last):
      ...
      ValueError: Cannot mix sequences and other types of rows without specifying a header
      >>> print(pretty_table([[1, 2], [3, 4, 5]]))
      Traceback (most recent call last):
      ...
      ValueError: Mismatched lengths.
      First row (len = 2):
      [1, 2]
      Current row (len = 3):
      [3, 4, 5]
      >>> print(pretty_table([{'a': 1, 'b': 2}], header='c d'))
      Traceback (most recent call last):
      ....
      KeyError: "Tried to access 'c', only keys are: ['a', 'b']"
      """
      rows2 = []
      if header:
          header = ensure_list_if_string(header)
          rows2.insert(0, header)
          row_type = ['any']
      else:
          header = []
          row_type = [None]
  
      def require_type(t):
          if row_type[0] not in (None, t, 'any'):
              raise ValueError('Cannot mix sequences and other types of rows without specifying a header')
          if row_type[0] is None:
              row_type[0] = t
  
      def handle_dict(d):
          require_type('mapping')
          if not header:
              header[:] = sorted(d.keys())
              rows2.insert(0, header)
          return [helpful_error_dict_get(d, key) for key in header]
  
      for row in rows:
          if isinstance(row, Mapping):
              row = handle_dict(row)
          elif isinstance(row, Sequence):
              require_type('sequence')
              if rows2 and len(row) != len(rows2[0]):
                  raise ValueError('Mismatched lengths.\n'
                                   'First row (len = %s):\n%s\n'
                                   'Current row (len = %s):\n%s' %
                                   (len(rows2[0]), rows2[0], len(row), row))
          else:
              row = handle_dict(row.__dict__)
          rows2.append(row)
  
      rows = [[str(cell) for cell in row] for row in rows2]
      widths = [max(len(row[i]) for row in rows) for i in range(len(rows[0]))]
      lines = [' | '.join(cell.ljust(width) for cell, width in zip(row, widths)).strip()
               for row in rows]
      if header:
          lines.insert(1, '-' * len(lines[0]))
      return '\n'.join(lines)