docs: def compare_notebooks(notebook_expected,
                        notebook_actual,
                        fmt=None,
                        allow_expected_differences=True,
                        raise_on_first_difference=True,
                        compare_outputs=False):
      """Compare the two notebooks, and raise with a meaningful message
      that explains the differences, if any"""
      fmt = long_form_one_format(fmt)
      format_name = fmt.get('format_name')
  
      # Expected differences
      allow_filtered_cell_metadata = allow_expected_differences
      allow_missing_code_cell_metadata = allow_expected_differences and format_name == 'sphinx'
      allow_missing_markdown_cell_metadata = allow_expected_differences and format_name in ['sphinx', 'spin']
      allow_removed_final_blank_line = allow_expected_differences
  
      cell_metadata_filter = notebook_actual.get('jupytext', {}).get('cell_metadata_filter')
  
      if format_name == 'sphinx' and notebook_actual.cells and notebook_actual.cells[0].source == '%matplotlib inline':
          notebook_actual.cells = notebook_actual.cells[1:]
  
      # Compare cells type and content
      test_cell_iter = iter(notebook_actual.cells)
      modified_cells = set()
      modified_cell_metadata = set()
      for i, ref_cell in enumerate(notebook_expected.cells, 1):
          try:
              test_cell = next(test_cell_iter)
          except StopIteration:
              if raise_on_first_difference:
                  raise NotebookDifference('No cell corresponding to {} cell #{}:\n{}'
                                           .format(ref_cell.cell_type, i, ref_cell.source))
              modified_cells.update(range(i, len(notebook_expected.cells) + 1))
              break
  
          ref_lines = [line for line in ref_cell.source.splitlines() if not _BLANK_LINE.match(line)]
          test_lines = []
  
          # 1. test cell type
          if ref_cell.cell_type != test_cell.cell_type:
              if raise_on_first_difference:
                  raise NotebookDifference("Unexpected cell type '{}' for {} cell #{}:\n{}"
                                           .format(test_cell.cell_type, ref_cell.cell_type, i, ref_cell.source))
              modified_cells.add(i)
  
          # 2. test cell metadata
          if (ref_cell.cell_type == 'code' and not allow_missing_code_cell_metadata) or \
                  (ref_cell.cell_type != 'code' and not allow_missing_markdown_cell_metadata):
  
              if allow_filtered_cell_metadata:
                  ref_cell.metadata = {key: ref_cell.metadata[key] for key in ref_cell.metadata
                                       if key not in _IGNORE_CELL_METADATA}
                  test_cell.metadata = {key: test_cell.metadata[key] for key in test_cell.metadata
                                        if key not in _IGNORE_CELL_METADATA}
  
              if ref_cell.metadata != test_cell.metadata:
                  if raise_on_first_difference:
                      try:
                          compare(ref_cell.metadata, test_cell.metadata)
                      except AssertionError as error:
                          raise NotebookDifference("Metadata differ on {} cell #{}: {}\nCell content:\n{}"
                                                   .format(test_cell.cell_type, i, str(error), ref_cell.source))
                  else:
                      modified_cell_metadata.update(set(test_cell.metadata).difference(ref_cell.metadata))
                      modified_cell_metadata.update(set(ref_cell.metadata).difference(test_cell.metadata))
                      for key in set(ref_cell.metadata).intersection(test_cell.metadata):
                          if ref_cell.metadata[key] != test_cell.metadata[key]:
                              modified_cell_metadata.add(key)
  
          test_lines.extend([line for line in test_cell.source.splitlines() if not _BLANK_LINE.match(line)])
  
          # 3. test cell content
          if ref_lines != test_lines:
              if raise_on_first_difference:
                  try:
                      compare('\n'.join(ref_lines), '\n'.join(test_lines))
                  except AssertionError as error:
                      raise NotebookDifference("Cell content differ on {} cell #{}: {}"
                                               .format(test_cell.cell_type, i, str(error)))
              else:
                  modified_cells.add(i)
  
          # 3. bis test entire cell content
          if not same_content(ref_cell.source, test_cell.source, allow_removed_final_blank_line):
              try:
                  compare(ref_cell.source, test_cell.source)
              except AssertionError as error:
                  if raise_on_first_difference:
                      raise NotebookDifference("Cell content differ on {} cell #{}: {}"
                                               .format(test_cell.cell_type, i, str(error)))
                  modified_cells.add(i)
  
          if not compare_outputs:
              continue
  
          if ref_cell.cell_type != 'code':
              continue
  
          ref_cell = filtered_cell(ref_cell,
                                   preserve_outputs=compare_outputs,
                                   cell_metadata_filter=cell_metadata_filter)
          test_cell = filtered_cell(test_cell,
                                    preserve_outputs=compare_outputs,
                                    cell_metadata_filter=cell_metadata_filter)
  
          try:
              compare(ref_cell, test_cell)
          except AssertionError as error:
              if raise_on_first_difference:
                  raise NotebookDifference("Cell outputs differ on {} cell #{}: {}"
                                           .format(test_cell['cell_type'], i, str(error)))
              modified_cells.add(i)
  
      # More cells in the actual notebook?
      remaining_cell_count = 0
      while True:
          try:
              test_cell = next(test_cell_iter)
              if raise_on_first_difference:
                  raise NotebookDifference('Additional {} cell: {}'.format(test_cell.cell_type, test_cell.source))
              remaining_cell_count += 1
          except StopIteration:
              break
  
      if remaining_cell_count and not raise_on_first_difference:
          modified_cells.update(
              range(len(notebook_expected.cells) + 1, len(notebook_expected.cells) + 1 + remaining_cell_count))
  
      # Compare notebook metadata
      modified_metadata = False
      try:
          compare(filtered_notebook_metadata(notebook_expected),
                  filtered_notebook_metadata(notebook_actual))
      except AssertionError as error:
          if raise_on_first_difference:
              raise NotebookDifference("Notebook metadata differ: {}".format(str(error)))
          modified_metadata = True
  
      error = []
      if modified_cells:
          error.append('Cells {} differ ({}/{})'.format(','.join([str(i) for i in modified_cells]),
                                                        len(modified_cells), len(notebook_expected.cells)))
      if modified_cell_metadata:
          error.append("Cell metadata '{}' differ".format("', '".join([str(i) for i in modified_cell_metadata])))
      if modified_metadata:
          error.append('Notebook metadata differ')
  
      if error:
          raise NotebookDifference(' | '.join(error))