Repository: mwouts/jupytext

Issue:
nbformat conflict with papermill
The latest version of papermill has pinned nbformat to >=5.1.2.  I noticed that up until 1.10.0 nbformat was pinned in this project as >=, but with 1.10.0 the version was pinned as <=5.0.8.  Is there a breaking change within with nbformat 5.1.x or was this pinning a typo and should be anything greater than 5.0.8?

https://github.com/nteract/papermill/blob/2.3.2/requirements.txt

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mwouts/jupytext/pull/736?src=pr&el=h1) Report
> Merging [#736](https://codecov.io/gh/mwouts/jupytext/pull/736?src=pr&el=desc) (e8bb9e2) into [master](https://codecov.io/gh/mwouts/jupytext/commit/4e9848948a9fab30a538080081d8f61d0d7bba76?el=desc) (4e98489) will **decrease** coverage by `1.50%`.
> The diff coverage is `97.61%`.

[![Impacted file tree graph](https://codecov.io/gh/mwouts/jupytext/pull/736/graphs/tree.svg?width=650&height=150&src=pr&token=a1pNjsiILi)](https://codecov.io/gh/mwouts/jupytext/pull/736?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #736      +/-   ##
==========================================
- Coverage   99.10%   97.59%   -1.51%     
==========================================
  Files         104      104              
  Lines        9866     9914      +48     
==========================================
- Hits         9778     9676     -102     
- Misses         88      238     +150     
```


| [Impacted Files](https://codecov.io/gh/mwouts/jupytext/pull/736?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [tests/test\_isort.py](https://codecov.io/gh/mwouts/jupytext/pull/736/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9pc29ydC5weQ==) | `100.00% <ø> (ø)` | |
| [tests/test\_pre\_commit\_5\_reformat\_markdown.py](https://codecov.io/gh/mwouts/jupytext/pull/736/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9wcmVfY29tbWl0XzVfcmVmb3JtYXRfbWFya2Rvd24ucHk=) | `28.12% <0.00%> (-71.88%)` | :arrow_down: |
| [tests/test\_preserve\_empty\_cells.py](https://codecov.io/gh/mwouts/jupytext/pull/736/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9wcmVzZXJ2ZV9lbXB0eV9jZWxscy5weQ==) | `100.00% <ø> (ø)` | |
| [jupytext/compare.py](https://codecov.io/gh/mwouts/jupytext/pull/736/diff?src=pr&el=tree#diff-anVweXRleHQvY29tcGFyZS5weQ==) | `98.71% <96.15%> (-0.64%)` | :arrow_down: |
| [jupytext/cli.py](https://codecov.io/gh/mwouts/jupytext/pull/736/diff?src=pr&el=tree#diff-anVweXRleHQvY2xpLnB5) | `96.19% <100.00%> (ø)` | |
| [jupytext/jupytext.py](https://codecov.io/gh/mwouts/jupytext/pull/736/diff?src=pr&el=tree#diff-anVweXRleHQvanVweXRleHQucHk=) | `96.23% <100.00%> (-2.10%)` | :arrow_down: |
| [tests/test\_active\_cells.py](https://codecov.io/gh/mwouts/jupytext/pull/736/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9hY3RpdmVfY2VsbHMucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/test\_black.py](https://codecov.io/gh/mwouts/jupytext/pull/736/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9ibGFjay5weQ==) | `100.00% <100.00%> (ø)` | |
| [tests/test\_cli.py](https://codecov.io/gh/mwouts/jupytext/pull/736/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9jbGkucHk=) | `94.14% <100.00%> (-5.86%)` | :arrow_down: |
| [tests/test\_cm\_config.py](https://codecov.io/gh/mwouts/jupytext/pull/736/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9jbV9jb25maWcucHk=) | `100.00% <100.00%> (ø)` | |
| ... and [21 more](https://codecov.io/gh/mwouts/jupytext/pull/736/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mwouts/jupytext/pull/736?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/mwouts/jupytext/pull/736?src=pr&el=footer). Last update [6abaea0...e8bb9e2](https://codecov.io/gh/mwouts/jupytext/pull/736?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml
index b7af3a4..700e252 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration.yml
@@ -178,7 +178,7 @@ jobs:
           run: |
             python -m pip install --upgrade pip
             # All dependencies but markdown-it-py
-            pip install 'nbformat<=5.0.8' pyyaml toml
+            pip install nbformat pyyaml toml
             pip install -r requirements-dev.txt
         - name: Install a Jupyter Kernel
           run: python -m ipykernel install --name python_kernel --user
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 436660c..a003e1c 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -7,6 +7,10 @@ Jupytext ChangeLog
 **Added**
 - The recursive glob pattern `**/*.ipynb` is now supported by Jupytext - Thanks to Banst for this contribution ([#731](https://github.com/mwouts/jupytext/issues/731))
 - Sage notebooks are supported. They can be converted to `.sage` and `.md` files and back. Thanks to Lars Franke for suggesting this! ([#727](https://github.com/mwouts/jupytext/issues/727))
+- Jupytext is also accessible with `python -m jupytext`. Thanks to Matthew Brett for his PR! ([#739](https://github.com/mwouts/jupytext/issues/739))
+
+**Changed**
+- We have tested Jupytext the new cell ids introduced in `nbformat>=5.1.0`. Cell ids are preserved by the `--sync` and `--update` command. So we removed the constraint on the version of `nbformat` ([#735](https://github.com/mwouts/jupytext/issues/735)).
 
 **Fixed**
 - We filtered out the `node_modules` folder from the `.tar.gz` package for Jupytext ([#730](https://github.com/mwouts/jupytext/issues/730))
diff --git a/environment-ci.yml b/environment-ci.yml
index 7f05f4e..bde84c5 100644
--- a/environment-ci.yml
+++ b/environment-ci.yml
@@ -8,7 +8,7 @@ dependencies:
   - jupyter
   - ipykernel
   - nbconvert
-  - nbformat<=5.0.8
+  - nbformat>=5.1.1
   - pyyaml
   - toml
   - markdown-it-py~=0.6.0
diff --git a/environment.yml b/environment.yml
index 5564d71..742e2ee 100644
--- a/environment.yml
+++ b/environment.yml
@@ -5,7 +5,7 @@ channels:
 dependencies:
   - python>=3.6
   - jupyterlab>=3.0
-  - nbformat<=5.0.8
+  - nbformat>=5.1.1
   - jupyter-packaging
   - pyyaml
   - toml
@@ -27,7 +27,7 @@ dependencies:
   - pip
   - setuptools
   - twine
-  - pandoc>=2.11
+  - pandoc>=2.11.4
   # make html in docs folder
   - sphinx
   - tox-conda
diff --git a/jupytext/__main__.py b/jupytext/__main__.py
new file mode 100644
index 0000000..c22c005
--- /dev/null
+++ b/jupytext/__main__.py
@@ -0,0 +1,13 @@
+""" Main for Jupytext
+
+Call with (e.g.)::
+
+    python -m jupytext my_notebook.ipynb --to Rmd
+"""
+
+import sys
+
+from .cli import jupytext
+
+if __name__ == "__main__":
+    sys.exit(jupytext())
diff --git a/jupytext/cli.py b/jupytext/cli.py
index 62ec5d2..7a03144 100644
--- a/jupytext/cli.py
+++ b/jupytext/cli.py
@@ -811,7 +811,7 @@ def jupytext_single_file(nb_file, args, log):
             check_file_version(notebook, nb_file, nb_dest)
             notebook = combine_inputs_with_outputs(notebook, read(nb_dest), fmt=fmt)
         elif os.path.isfile(nb_dest):
-            action = " (destination file replaced)"
+            action = " (destination file replaced [use --update to preserve cell outputs and ids])"
         else:
             action = ""
 
diff --git a/jupytext/compare.py b/jupytext/compare.py
index ffe3b95..aacd0b2 100644
--- a/jupytext/compare.py
+++ b/jupytext/compare.py
@@ -110,26 +110,13 @@ def compare_notebooks(
     allow_expected_differences=True,
     raise_on_first_difference=True,
     compare_outputs=False,
+    compare_ids=None,
 ):
     """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
@@ -137,11 +124,80 @@ def compare_notebooks(
     ):
         notebook_actual.cells = notebook_actual.cells[1:]
 
-    # Compare cells type and content
-    test_cell_iter = iter(notebook_actual.cells)
+    if compare_ids is None:
+        compare_ids = compare_outputs
+
+    modified_cells, modified_cell_metadata = compare_cells(
+        notebook_actual.cells,
+        notebook_expected.cells,
+        raise_on_first_difference,
+        compare_outputs=compare_outputs,
+        compare_ids=compare_ids,
+        cell_metadata_filter=notebook_actual.get("jupytext", {}).get(
+            "cell_metadata_filter"
+        ),
+        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_filtered_cell_metadata=allow_expected_differences,
+        allow_removed_final_blank_line=allow_expected_differences,
+    )
+
+    # Compare notebook metadata
+    modified_metadata = False
+    try:
+        compare(
+            filtered_notebook_metadata(notebook_actual),
+            filtered_notebook_metadata(notebook_expected),
+        )
+    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))
+
+
+def compare_cells(
+    actual_cells,
+    expected_cells,
+    raise_on_first_difference=True,
+    compare_outputs=True,
+    compare_ids=True,
+    cell_metadata_filter=None,
+    allow_missing_code_cell_metadata=False,
+    allow_missing_markdown_cell_metadata=False,
+    allow_filtered_cell_metadata=False,
+    allow_removed_final_blank_line=False,
+):
+    """Compare two collection of notebook cells"""
+    test_cell_iter = iter(actual_cells)
     modified_cells = set()
     modified_cell_metadata = set()
-    for i, ref_cell in enumerate(notebook_expected.cells, 1):
+
+    for i, ref_cell in enumerate(expected_cells, 1):
         try:
             test_cell = next(test_cell_iter)
         except StopIteration:
@@ -151,7 +207,7 @@ def compare_notebooks(
                         ref_cell.cell_type, i, ref_cell.source
                     )
                 )
-            modified_cells.update(range(i, len(notebook_expected.cells) + 1))
+            modified_cells.update(range(i, len(expected_cells) + 1))
             break
 
         ref_lines = [
@@ -169,6 +225,15 @@ def compare_notebooks(
                 )
             modified_cells.add(i)
 
+        # Compare cell ids (introduced in nbformat 5.1.0)
+        if compare_ids and test_cell.get("id") != ref_cell.get("id"):
+            if raise_on_first_difference:
+                raise NotebookDifference(
+                    f"Cell ids differ on {test_cell['cell_type']} cell #{i}: "
+                    f"'{test_cell.get('id')}' != '{ref_cell.get('id')}'"
+                )
+            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
@@ -291,43 +356,12 @@ def compare_notebooks(
     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_actual),
-            filtered_notebook_metadata(notebook_expected),
-        )
-    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),
+                len(expected_cells) + 1,
+                len(expected_cells) + 1 + remaining_cell_count,
             )
         )
-    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))
+    return modified_cells, modified_cell_metadata
 
 
 def test_round_trip_conversion(
diff --git a/jupytext/jupytext.py b/jupytext/jupytext.py
index c837fc3..bc382bb 100644
--- a/jupytext/jupytext.py
+++ b/jupytext/jupytext.py
@@ -443,7 +443,7 @@ def writes(notebook, fmt, version=nbformat.NO_CONVERT, **kwargs):
             f"Please convert your notebooks to nbformat version 4 with "
             f"'jupyter nbconvert --to notebook --inplace', or call this function with 'version=4'."
         )
-    if version > 4 or (version == 4 and version_minor > 4):
+    if version > 4 or (version == 4 and version_minor > 5):
         warnings.warn(
             f"Notebooks in nbformat version {version}.{version_minor} "
             f"have not been tested with Jupytext version {__version__}."
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 9c65a5f..a794856 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,3 +1,4 @@
+nbformat>=5.1.1
 flake8
 isort
 black
diff --git a/requirements.txt b/requirements.txt
index db23ba8..19a77f3 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,4 @@
-nbformat<=5.0.8
+nbformat
 pyyaml
 toml
 markdown-it-py~=0.6.0
diff --git a/setup.py b/setup.py
index 28d41a8..4845833 100644
--- a/setup.py
+++ b/setup.py
@@ -32,7 +32,7 @@ setup_args = dict(
     packages=find_packages(exclude=["tests"]),
     entry_points={"console_scripts": ["jupytext = jupytext.cli:jupytext"]},
     tests_require=["pytest"],
-    install_requires=["nbformat<=5.0.8", "pyyaml", "toml", "markdown-it-py~=0.6.0"],
+    install_requires=["nbformat", "pyyaml", "toml", "markdown-it-py~=0.6.0"],
     python_requires="~=3.6",
     extras_require={
         # left for back-compatibility


Test patch:
diff --git a/tests/test_active_cells.py b/tests/test_active_cells.py
index be1ffa4..2982419 100644
--- a/tests/test_active_cells.py
+++ b/tests/test_active_cells.py
@@ -1,7 +1,8 @@
 import pytest
+from nbformat import NotebookNode
 
 import jupytext
-from jupytext.compare import compare
+from jupytext.compare import compare, compare_cells
 
 HEADER = {
     ".py": """# ---
@@ -64,7 +65,8 @@ def check_active_cell(ext, active_dict):
     nb = jupytext.reads(text, ext)
     assert len(nb.cells) == 1
     compare(jupytext.writes(nb, ext), text)
-    compare(nb.cells[0], active_dict[".ipynb"])
+    cell = NotebookNode(active_dict[".ipynb"])
+    compare_cells(nb.cells, [cell], compare_ids=False)
 
 
 @pytest.mark.parametrize("ext", [".Rmd", ".md", ".py", ".R"])
@@ -159,7 +161,8 @@ def test_active_ipynb_rspin(no_jupytext_version_number):
     nb = jupytext.reads(ACTIVE_IPYNB_RSPIN[".R"], "R:spin")
     assert len(nb.cells) == 1
     compare(jupytext.writes(nb, "R:spin"), ACTIVE_IPYNB_RSPIN[".R"])
-    compare(nb.cells[0], ACTIVE_IPYNB_RSPIN[".ipynb"])
+    cell = NotebookNode(ACTIVE_IPYNB_RSPIN[".ipynb"])
+    compare_cells(nb.cells, [cell], compare_ids=False)
 
 
 ACTIVE_PY_IPYNB = {
diff --git a/tests/test_black.py b/tests/test_black.py
index 56cd2d4..7cdcb53 100644
--- a/tests/test_black.py
+++ b/tests/test_black.py
@@ -7,26 +7,24 @@ from nbformat.v4.nbbase import new_code_cell, new_notebook
 from jupytext import read, write
 from jupytext.cli import jupytext, pipe_notebook, system
 from jupytext.combine import black_invariant
-from jupytext.compare import compare
+from jupytext.compare import compare, compare_cells, compare_notebooks
 from jupytext.header import _DEFAULT_NOTEBOOK_METADATA
 
 from .utils import list_notebooks, requires_autopep8, requires_black, requires_flake8
 
 
 @requires_black
-@pytest.mark.parametrize("nb_file", list_notebooks("ipynb_py"))
-def test_apply_black_on_python_notebooks(nb_file, tmpdir):
-    tmp_ipynb = str(tmpdir.join("notebook.ipynb"))
-    tmp_py = str(tmpdir.join("notebook.py"))
-    copyfile(nb_file, tmp_ipynb)
+@pytest.mark.parametrize("nb_file", list_notebooks("ipynb_py")[:1])
+def test_apply_black_on_python_notebooks(tmpdir, cwd_tmpdir, nb_file):
+    copyfile(nb_file, "notebook.ipynb")
 
-    jupytext(args=[tmp_ipynb, "--to", "py:percent"])
-    system("black", tmp_py)
-    jupytext(args=[tmp_py, "--to", "ipynb", "--update"])
+    jupytext(args=["notebook.ipynb", "--to", "py:percent"])
+    system("black", "notebook.py")
+    jupytext(args=["notebook.py", "--to", "ipynb", "--update"])
 
     nb1 = read(nb_file)
-    nb2 = read(tmp_ipynb)
-    nb3 = read(tmp_py)
+    nb2 = read("notebook.ipynb")
+    nb3 = read("notebook.py")
 
     assert len(nb1.cells) == len(nb2.cells)
     assert len(nb1.cells) == len(nb3.cells)
@@ -57,20 +55,24 @@ def test_black_invariant():
 
 @requires_black
 def test_pipe_into_black():
-    nb_org = new_notebook(cells=[new_code_cell("1        +1")])
-    nb_dest = new_notebook(cells=[new_code_cell("1 + 1")])
+    nb_org = new_notebook(cells=[new_code_cell("1        +1", id="cell-id")])
+    nb_dest = new_notebook(cells=[new_code_cell("1 + 1", id="cell-id")])
 
     nb_pipe = pipe_notebook(nb_org, "black")
-    compare(nb_pipe, nb_dest)
+    compare_notebooks(
+        nb_pipe, nb_dest, allow_expected_differences=False, compare_ids=True
+    )
 
 
 @requires_autopep8
 def test_pipe_into_autopep8():
-    nb_org = new_notebook(cells=[new_code_cell("1        +1")])
-    nb_dest = new_notebook(cells=[new_code_cell("1 + 1")])
+    nb_org = new_notebook(cells=[new_code_cell("1        +1", id="cell-id")])
+    nb_dest = new_notebook(cells=[new_code_cell("1 + 1", id="cell-id")])
 
     nb_pipe = pipe_notebook(nb_org, "autopep8 -")
-    compare(nb_pipe, nb_dest)
+    compare_notebooks(
+        nb_pipe, nb_dest, allow_expected_differences=False, compare_ids=True
+    )
 
 
 @requires_flake8
@@ -91,20 +93,21 @@ def test_apply_black_through_jupytext(tmpdir, nb_file):
     # Load real notebook metadata to get the 'auto' extension in --pipe-fmt to work
     metadata = read(nb_file).metadata
 
-    nb_org = new_notebook(cells=[new_code_cell("1        +1")], metadata=metadata)
-    nb_black = new_notebook(cells=[new_code_cell("1 + 1")], metadata=metadata)
-
-    os.makedirs(str(tmpdir.join("notebook_folder")))
-    os.makedirs(str(tmpdir.join("script_folder")))
+    nb_org = new_notebook(
+        cells=[new_code_cell("1        +1", id="cell-id")], metadata=metadata
+    )
+    nb_black = new_notebook(
+        cells=[new_code_cell("1 + 1", id="cell-id")], metadata=metadata
+    )
 
-    tmp_ipynb = str(tmpdir.join("notebook_folder").join("notebook.ipynb"))
-    tmp_py = str(tmpdir.join("script_folder").join("notebook.py"))
+    tmp_ipynb = str(tmpdir.mkdir("notebook_folder").join("notebook.ipynb"))
+    tmp_py = str(tmpdir.mkdir("script_folder").join("notebook.py"))
 
     # Black in place
     write(nb_org, tmp_ipynb)
     jupytext([tmp_ipynb, "--pipe", "black"])
     nb_now = read(tmp_ipynb)
-    compare(nb_now, nb_black)
+    compare_notebooks(nb_now, nb_black, compare_ids=True)
 
     # Write to another folder using dots
     script_fmt = os.path.join("..", "script_folder//py:percent")
@@ -113,7 +116,7 @@ def test_apply_black_through_jupytext(tmpdir, nb_file):
     assert os.path.isfile(tmp_py)
     nb_now = read(tmp_py)
     nb_now.metadata = metadata
-    compare(nb_now, nb_black)
+    compare_notebooks(nb_now, nb_black)
     os.remove(tmp_py)
 
     # Map to another folder based on file name
@@ -134,39 +137,40 @@ def test_apply_black_through_jupytext(tmpdir, nb_file):
     assert os.path.isfile(tmp_py)
     nb_now = read(tmp_py)
     nb_now.metadata = metadata
-    compare(nb_now, nb_black)
+    compare_notebooks(nb_now, nb_black)
 
 
 @requires_black
-@pytest.mark.parametrize("nb_file", list_notebooks("ipynb_py"))
-def test_apply_black_and_sync_on_paired_notebook(tmpdir, nb_file):
+@pytest.mark.parametrize("nb_file", list_notebooks("ipynb_py")[:1])
+def test_apply_black_and_sync_on_paired_notebook(tmpdir, cwd_tmpdir, nb_file):
     # Load real notebook metadata to get the 'auto' extension in --pipe-fmt to work
     metadata = read(nb_file).metadata
     metadata["jupytext"] = {"formats": "ipynb,py"}
     assert "language_info" in metadata
 
-    nb_org = new_notebook(cells=[new_code_cell("1        +1")], metadata=metadata)
-    nb_black = new_notebook(cells=[new_code_cell("1 + 1")], metadata=metadata)
-
-    tmp_ipynb = str(tmpdir.join("notebook.ipynb"))
-    tmp_py = str(tmpdir.join("notebook.py"))
+    nb_org = new_notebook(
+        cells=[new_code_cell("1        +1", id="cell-id")], metadata=metadata
+    )
+    nb_black = new_notebook(
+        cells=[new_code_cell("1 + 1", id="cell-id")], metadata=metadata
+    )
 
     # Black in place
-    write(nb_org, tmp_ipynb)
-    jupytext([tmp_ipynb, "--pipe", "black", "--sync"])
+    write(nb_org, "notebook.ipynb")
+    jupytext(["notebook.ipynb", "--pipe", "black", "--sync"])
 
-    nb_now = read(tmp_ipynb)
-    compare(nb_now, nb_black)
+    nb_now = read("notebook.ipynb")
+    compare_notebooks(nb_now, nb_black, compare_ids=True)
     assert "language_info" in nb_now.metadata
 
-    nb_now = read(tmp_py)
+    nb_now = read("notebook.py")
     nb_now.metadata["jupytext"].pop("text_representation")
     nb_black.metadata = {
         key: nb_black.metadata[key]
         for key in nb_black.metadata
         if key in _DEFAULT_NOTEBOOK_METADATA.split(",")
     }
-    compare(nb_now, nb_black)
+    compare_notebooks(nb_now, nb_black)
 
 
 @requires_black
@@ -202,7 +206,7 @@ jupyter:
     jupytext([tmp_md, "--pipe", "black"])
 
     nb = read(tmp_md)
-    compare(nb.cells, [new_code_cell("1 + 2 + 3 + 4")])
+    compare_cells(nb.cells, [new_code_cell("1 + 2 + 3 + 4")], compare_ids=False)
 
 
 @requires_black
diff --git a/tests/test_cli.py b/tests/test_cli.py
index e506dff..46e97e8 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -8,6 +8,7 @@ import unittest.mock as mock
 from argparse import ArgumentTypeError
 from io import StringIO
 from shutil import copyfile
+from subprocess import check_call
 
 import nbformat
 import pytest
@@ -65,6 +66,24 @@ def test_convert_single_file_in_place(nb_file, tmpdir):
     compare_notebooks(nb2, nb1)
 
 
+def test_convert_single_file_in_place_m(tmpdir):
+    nb_file = list_notebooks("ipynb_py")[0]
+    nb_org = str(tmpdir.join(os.path.basename(nb_file)))
+    copyfile(nb_file, nb_org)
+
+    base, ext = os.path.splitext(nb_org)
+
+    for fmt_ext in ("py", "Rmd"):
+        nb_other = base + "." + fmt_ext
+
+        check_call([sys.executable, "-m", "jupytext", nb_org, "--to", fmt_ext])
+
+        nb1 = read(nb_org)
+        nb2 = read(nb_other)
+
+        compare_notebooks(nb2, nb1)
+
+
 @pytest.mark.parametrize("nb_file", list_notebooks("ipynb") + list_notebooks("Rmd"))
 def test_convert_single_file(nb_file, tmpdir, capsys):
     nb_org = str(tmpdir.join(os.path.basename(nb_file)))
@@ -1142,10 +1161,23 @@ def test_jupytext_set_formats_file_gives_an_informative_error(tmpdir, cwd_tmpdir
     nb_file = tmpdir.join("notebook.ipynb")
     md_file.write("Some text")
 
-    with pytest.warns(None) as record:
+    with pytest.warns(None) as warnings:
         jupytext(["--sync", "notebook.md"])
 
-    assert len(record) == 0
+    # TODO: remove this filter when the fix for
+    #  https://github.com/jupyter/nbformat/issues/212
+    #  is released
+    warnings = [
+        w
+        for w in warnings
+        if not (
+            "Sampling from a set deprecated" in str(w.message)
+            and "nbformat" in w.filename
+        )
+    ]
+
+    assert not warnings
+
     assert py_file.exists()
     assert nb_file.exists()
 
@@ -1192,3 +1224,55 @@ def test_glob_recursive(tmpdir, cwd_tmpdir):
 
     assert tmpdir.join("test.ipynb").isfile()
     assert tmpdir.join("subfolder").join("test.ipynb").isfile()
+
+
+def test_jupytext_sync_preserves_cell_ids(tmpdir, cwd_tmpdir, notebook_with_outputs):
+    write(notebook_with_outputs, "test.ipynb")
+
+    # Sync with a py file, this should not change the cell id
+    jupytext(["--set-formats", "ipynb,py:percent", "test.ipynb"])
+    nb = read("test.ipynb")
+    assert nb.cells[0].source == "1+1"
+    assert nb.cells[0].id == notebook_with_outputs.cells[0].id
+
+    # Change the py file and sync. This should not change the cell id neither
+    py_nb = tmpdir.join("test.py")
+    py = py_nb.read()
+    py_nb.write(py.replace("1+1", "2+2"))
+
+    jupytext(["--sync", "test.ipynb"])
+    nb = read("test.ipynb")
+    assert nb.cells[0].source == "2+2"
+    assert nb.cells[0].id == notebook_with_outputs.cells[0].id
+
+
+def test_jupytext_update_preserves_cell_ids(tmpdir, cwd_tmpdir, notebook_with_outputs):
+    write(notebook_with_outputs, "test.ipynb")
+
+    # Sync with a py file, this should not change the cell id
+    jupytext(["--to", "py:percent", "test.ipynb"])
+    nb = read("test.ipynb")
+    assert nb.cells[0].source == "1+1"
+    assert nb.cells[0].id == notebook_with_outputs.cells[0].id
+
+    # Change the py file and update the ipynb file.
+    # This should not change the cell id neither
+    py_nb = tmpdir.join("test.py")
+    py = py_nb.read()
+    py_nb.write(py.replace("1+1", "2+2"))
+
+    jupytext(["--to", "notebook", "--update", "test.py"])
+    nb = read("test.ipynb")
+    assert nb.cells[0].source == "2+2"
+    assert nb.cells[0].id == notebook_with_outputs.cells[0].id
+
+
+def test_jupytext_to_ipynb_suggests_update(tmpdir, cwd_tmpdir, capsys):
+    tmpdir.join("test.py").write("1 + 1\n")
+    jupytext(["--to", "ipynb", "test.py"])
+    capture = capsys.readouterr()
+    assert "update" not in capture.out
+
+    jupytext(["--to", "ipynb", "test.py"])
+    capture = capsys.readouterr()
+    assert "update" in capture.out
diff --git a/tests/test_cm_config.py b/tests/test_cm_config.py
index 76a63b5..3fff9d8 100644
--- a/tests/test_cm_config.py
+++ b/tests/test_cm_config.py
@@ -8,6 +8,7 @@ from nbformat.v4.nbbase import new_code_cell, new_markdown_cell, new_notebook
 from tornado.web import HTTPError
 
 import jupytext
+from jupytext.compare import compare_cells
 
 from .utils import notebook_model
 
@@ -170,4 +171,4 @@ def test_paired_files_and_symbolic_links(tmpdir):
     # Reload and make sure that we get the updated notebook
     model = cm.get("link_to_notebooks/notebook.ipynb")
     nb = model["content"]
-    assert nb.cells == [new_code_cell("3 + 3")]
+    compare_cells(nb.cells, [new_code_cell("3 + 3")], compare_ids=False)
diff --git a/tests/test_compare.py b/tests/test_compare.py
index 4f53598..66e644c 100644
--- a/tests/test_compare.py
+++ b/tests/test_compare.py
@@ -6,7 +6,12 @@ from nbformat.v4.nbbase import (
     new_raw_cell,
 )
 
-from jupytext.compare import NotebookDifference, compare, compare_notebooks
+from jupytext.compare import (
+    NotebookDifference,
+    compare,
+    compare_cells,
+    compare_notebooks,
+)
 from jupytext.compare import test_round_trip_conversion as round_trip_conversion
 
 
@@ -47,9 +52,9 @@ def notebook_expected():
     return new_notebook(
         metadata=notebook_metadata(),
         cells=[
-            new_markdown_cell("First markdown cell"),
-            new_code_cell("1 + 1"),
-            new_markdown_cell("Second markdown cell"),
+            new_markdown_cell("First markdown cell", id="markdown-cell-one"),
+            new_code_cell("1 + 1", id="code-cell-one"),
+            new_markdown_cell("Second markdown cell", id="markdown-cell-two"),
         ],
     )
 
@@ -61,9 +66,9 @@ def notebook_actual():
     return new_notebook(
         metadata=metadata,
         cells=[
-            new_markdown_cell("First markdown cell"),
-            new_code_cell("1 + 1"),
-            new_markdown_cell("Modified markdown cell"),
+            new_markdown_cell("First markdown cell", id="markdown-cell-one"),
+            new_code_cell("1 + 1", id="code-cell-one"),
+            new_markdown_cell("Modified markdown cell", id="markdown-cell-two"),
         ],
     )
 
@@ -77,16 +82,16 @@ def test_compare_on_notebooks(notebook_actual, notebook_expected):
         == """
 --- expected
 +++ actual
-@@ -15,7 +15,7 @@
-   {
+@@ -18,7 +18,7 @@
     "cell_type": "markdown",
+    "id": "markdown-cell-two",
     "metadata": {},
 -   "source": "Second markdown cell"
 +   "source": "Modified markdown cell"
    }
   ],
   "metadata": {
-@@ -34,7 +34,7 @@
+@@ -37,7 +37,7 @@
     "name": "python",
     "nbconvert_exporter": "python",
     "pygments_lexer": "ipython3",
@@ -300,3 +305,10 @@ def test_notebook_metadata_differ():
     with pytest.raises(NotebookDifference) as exception_info:
         compare_notebooks(nb2, nb1, raise_on_first_difference=False)
     assert "Notebook metadata differ" in exception_info.value.args[0]
+
+
+def test_cell_ids_differ():
+    with pytest.raises(NotebookDifference, match="'id-one' != 'id-two'"):
+        compare_cells(
+            [new_code_cell("1", id="id-one")], [new_code_cell("1", id="id-two")]
+        )
diff --git a/tests/test_contentsmanager.py b/tests/test_contentsmanager.py
index aeb506a..a219b42 100644
--- a/tests/test_contentsmanager.py
+++ b/tests/test_contentsmanager.py
@@ -13,7 +13,7 @@ from tornado.web import HTTPError
 
 import jupytext
 from jupytext.cli import jupytext as jupytext_cli
-from jupytext.compare import compare, compare_notebooks
+from jupytext.compare import compare, compare_cells, compare_notebooks
 from jupytext.formats import auto_ext_from_metadata, read_format_from_metadata
 from jupytext.header import header_to_metadata_and_cell
 from jupytext.jupytext import read, write, writes
@@ -1792,7 +1792,9 @@ def test_jupytext_jupyter_fs_metamanager(tmpdir):
     model = cm.get(osfs + ":text.md", type="notebook")
     assert model["type"] == "notebook"
     # We only compare the cells, as kernelspecs are added to the notebook metadata
-    compare(model["content"].cells, [new_markdown_cell(text.strip())])
+    compare_cells(
+        model["content"].cells, [new_markdown_cell(text.strip())], compare_ids=False
+    )
 
     for nb_file in ["notebook.ipynb", "text_notebook.md"]:
         model = cm.get(osfs + ":" + nb_file)
@@ -1802,7 +1804,7 @@ def test_jupytext_jupyter_fs_metamanager(tmpdir):
         # saving adds 'trusted=True' to the code cell metadata
         for cell in actual_cells:
             cell.metadata = {}
-        compare(actual_cells, nb.cells)
+        compare_cells(actual_cells, nb.cells, compare_ids=False)
 
 
 def test_config_jupytext_jupyter_fs_meta_manager(tmpdir):
diff --git a/tests/test_mirror.py b/tests/test_mirror.py
index d051dde..4c83d7e 100644
--- a/tests/test_mirror.py
+++ b/tests/test_mirror.py
@@ -61,7 +61,7 @@ def assert_conversion_same_as_mirror(nb_file, fmt, mirror_name, compare_notebook
         # Read and convert the mirror file to the latest nbformat version if necessary
         nb_mirror = jupytext.read(mirror_file, as_version=notebook.nbformat)
         nb_mirror.nbformat_minor = notebook.nbformat_minor
-        compare(nb_mirror, notebook)
+        compare_notebooks(nb_mirror, notebook)
         return
     elif ext == ".ipynb":
         notebook = jupytext.read(mirror_file)
diff --git a/tests/test_nbformat_version.py b/tests/test_nbformat_version.py
index 881cc69..b2ea57a 100644
--- a/tests/test_nbformat_version.py
+++ b/tests/test_nbformat_version.py
@@ -25,10 +25,9 @@ def sample_notebook_v3_json(sample_notebook_v3):
 
 
 @pytest.fixture()
-def sample_notebook_v4_5():
+def sample_notebook_v4_99():
     nb = new_notebook_v4(cells=[new_markdown_cell("Hi")])
-    nb["nbformat_minor"] = 5
-    nb["cells"][0]["id"] = "unique-id"
+    nb["nbformat_minor"] = 99
     return nb
 
 
@@ -57,11 +56,11 @@ def test_jupytext_gives_a_meaningful_error_when_writing_nbformat_3(
 
 
 @pytest.mark.parametrize("fmt", ["py:light", "py:percent", "md"])
-def test_jupytext_gives_a_meaningful_error_when_writing_nbformat_4_5(
-    sample_notebook_v4_5, fmt
+def test_jupytext_gives_a_meaningful_error_when_writing_nbformat_4_99(
+    sample_notebook_v4_99, fmt
 ):
     with pytest.warns(
         Warning,
-        match="Notebooks in nbformat version 4.5 have not been tested",
+        match="Notebooks in nbformat version 4.99 have not been tested",
     ):
-        writes(sample_notebook_v4_5, fmt=fmt)
+        writes(sample_notebook_v4_99, fmt=fmt)
diff --git a/tests/test_pre_commit_0_ipynb_to_py.py b/tests/test_pre_commit_0_ipynb_to_py.py
index b165224..5bd6e22 100644
--- a/tests/test_pre_commit_0_ipynb_to_py.py
+++ b/tests/test_pre_commit_0_ipynb_to_py.py
@@ -5,6 +5,7 @@ from pre_commit.main import main as pre_commit
 
 from jupytext import read, write
 from jupytext.cli import jupytext
+from jupytext.compare import compare_cells
 
 from .utils import skip_pre_commit_tests_on_windows
 
@@ -66,4 +67,4 @@ repos:
 
     # But it won't change the ipynb file (if you want that, use the --sync mode)
     nb = read("test.ipynb")
-    assert nb.cells == [new_markdown_cell("Some other text")]
+    compare_cells(nb.cells, [new_markdown_cell("Some other text")], compare_ids=False)
diff --git a/tests/test_pre_commit_5_reformat_markdown.py b/tests/test_pre_commit_5_reformat_markdown.py
index 7f923d5..9eec006 100644
--- a/tests/test_pre_commit_5_reformat_markdown.py
+++ b/tests/test_pre_commit_5_reformat_markdown.py
@@ -19,7 +19,12 @@ def test_pre_commit_hook_sync_reformat_code_and_markdown(
     notebook_with_outputs,
 ):
     """Here we sync the ipynb notebook with a py:percent file and also apply black and pandoc to reformat both
-    code and markdown cells."""
+    code and markdown cells.
+
+    Note: the new cell ids introduced in nbformat 5.1.0 are not yet supported by all the programs that treat
+    ipynb files. Consequently we pin the version of nbformat to 5.0.8 in all the environments below (and you
+    will have to do the same on the environment in which you edit the notebooks).
+    """
     pre_commit_config_yaml = f"""
 repos:
 - repo: {jupytext_repo_root}
@@ -27,10 +32,13 @@ repos:
   hooks:
   - id: jupytext
     args: [--sync, --pipe-fmt, ipynb, --pipe, 'pandoc --from ipynb --to ipynb --markdown-headings=atx', --diff]
+    additional_dependencies:
+    - nbformat==5.0.8  # because pandoc 2.11.4 does not preserve yet the new cell ids
   - id: jupytext
     args: [--sync, --pipe, black, --diff]
     additional_dependencies:
-    - black==20.8b1  # Matches hook
+    - black==20.8b1  # Matches black hook below
+    - nbformat==5.0.8  # for compatibility with the pandoc hook above
 
 - repo: https://github.com/psf/black
   rev: 20.8b1
@@ -71,6 +79,10 @@ And a VERY long line.
         ],
         metadata=notebook_with_outputs.metadata,
     )
+
+    # We write the notebook in version 4.4, i.e. with the equivalent of nbformat version 5.0.8
+    notebook.nbformat = 4
+    notebook.nbformat_minor = 4
     write(notebook, "test.ipynb")
 
     # try to commit it, should fail because
diff --git a/tests/test_pre_commit_scripts.py b/tests/test_pre_commit_scripts.py
index ab75b716..e5bf907 100644
--- a/tests/test_pre_commit_scripts.py
+++ b/tests/test_pre_commit_scripts.py
@@ -9,7 +9,7 @@ from nbformat.v4.nbbase import new_code_cell, new_markdown_cell, new_notebook
 
 from jupytext import read, write
 from jupytext.cli import jupytext, system
-from jupytext.compare import compare, compare_notebooks
+from jupytext.compare import compare_cells, compare_notebooks
 
 from .utils import (
     list_notebooks,
@@ -139,7 +139,9 @@ def test_sync_with_pre_commit_hook(tmpdir):
     assert "notebook.md" in git("ls-tree", "-r", "master", "--name-only")
 
     nb = read(tmp_ipynb)
-    compare(nb.cells, [new_markdown_cell("Notebook was edited")])
+    compare_cells(
+        nb.cells, [new_markdown_cell("Notebook was edited")], compare_ids=False
+    )
 
     # create and commit a jpg file
     tmp_jpg = str(tmpdir.join("image.jpg"))
diff --git a/tests/test_preserve_empty_cells.py b/tests/test_preserve_empty_cells.py
index 7fea5cb..4c1e5eb 100644
--- a/tests/test_preserve_empty_cells.py
+++ b/tests/test_preserve_empty_cells.py
@@ -22,9 +22,9 @@ def test_file_with_blank_lines(blank_lines):
 def test_notebook_with_empty_cells(blank_cells):
     notebook = new_notebook(
         cells=[new_markdown_cell("markdown cell one")]
-        + [new_code_cell("")] * blank_cells
+        + [new_code_cell("") for i in range(blank_cells)]
         + [new_markdown_cell("markdown cell two")]
-        + [new_code_cell("")] * blank_cells,
+        + [new_code_cell("") for i in range(blank_cells)],
         metadata={"jupytext": {"main_language": "python"}},
     )
 
diff --git a/tests/test_read_simple_markdown.py b/tests/test_read_simple_markdown.py
index cef10c5..9ad0ab0 100644
--- a/tests/test_read_simple_markdown.py
+++ b/tests/test_read_simple_markdown.py
@@ -7,7 +7,7 @@ from nbformat.v4.nbbase import (
 
 import jupytext
 from jupytext.combine import combine_inputs_with_outputs
-from jupytext.compare import compare, compare_notebooks
+from jupytext.compare import compare, compare_cells, compare_notebooks
 
 
 def test_read_mostly_py_markdown_file(
@@ -56,32 +56,14 @@ cat(stringi::stri_rand_lipsum(3), sep='\n\n')
 ):
     nb = jupytext.reads(markdown, "md")
     assert nb.metadata["jupytext"]["main_language"] == "python"
-    compare(
+    compare_cells(
         nb.cells,
         [
-            {
-                "cell_type": "raw",
-                "source": "---\ntitle: Simple file\n---",
-                "metadata": {},
-            },
-            {
-                "cell_type": "code",
-                "metadata": {},
-                "execution_count": None,
-                "source": "import numpy as np\n" "x = np.arange(0, 2*math.pi, eps)",
-                "outputs": [],
-            },
-            {
-                "cell_type": "code",
-                "metadata": {},
-                "execution_count": None,
-                "source": "x = np.arange(0,1,eps)\ny = np.abs(x)-.5",
-                "outputs": [],
-            },
-            {
-                "cell_type": "markdown",
-                "metadata": {},
-                "source": """This is
+            new_raw_cell("---\ntitle: Simple file\n---"),
+            new_code_cell("import numpy as np\n" "x = np.arange(0, 2*math.pi, eps)"),
+            new_code_cell("x = np.arange(0,1,eps)\ny = np.abs(x)-.5"),
+            new_markdown_cell(
+                """This is
 a Markdown cell
 
 ```
@@ -95,24 +77,13 @@ a Markdown cell
 # with two blank lines
 ```
 
-And the same markdown cell continues""",
-            },
-            {"cell_type": "raw", "metadata": {}, "source": "this is a raw cell"},
-            {
-                "cell_type": "code",
-                "metadata": {},
-                "execution_count": None,
-                "source": "%%R\nls()",
-                "outputs": [],
-            },
-            {
-                "cell_type": "code",
-                "metadata": {},
-                "execution_count": None,
-                "source": "%%R\ncat(stringi::" "stri_rand_lipsum(3), sep='\n\n')",
-                "outputs": [],
-            },
+And the same markdown cell continues"""
+            ),
+            new_raw_cell("this is a raw cell"),
+            new_code_cell("%%R\nls()"),
+            new_code_cell("%%R\ncat(stringi::" "stri_rand_lipsum(3), sep='\n\n')"),
         ],
+        compare_ids=False,
     )
 
     markdown2 = jupytext.writes(nb, "md")
@@ -141,7 +112,7 @@ cell
 ):
     nb = jupytext.reads(markdown, "md")
     assert nb.metadata["jupytext"]["main_language"] == "python"
-    compare(
+    compare_cells(
         nb.cells,
         [
             new_markdown_cell("Some text"),
@@ -162,6 +133,7 @@ cell""",
                 metadata={"region_name": "markdown"},
             ),
         ],
+        compare_ids=False,
     )
 
     markdown2 = jupytext.writes(nb, "md")
@@ -180,24 +152,13 @@ cat(stringi::stri_rand_lipsum(3), sep='\n\n')
 ):
     nb = jupytext.reads(markdown, "md")
     assert nb.metadata["jupytext"]["main_language"] == "R"
-    compare(
+    compare_cells(
         nb.cells,
         [
-            {
-                "cell_type": "code",
-                "metadata": {},
-                "execution_count": None,
-                "source": "ls()",
-                "outputs": [],
-            },
-            {
-                "cell_type": "code",
-                "metadata": {},
-                "execution_count": None,
-                "source": "cat(stringi::stri_rand_lipsum(3), sep='\n\n')",
-                "outputs": [],
-            },
+            new_code_cell("ls()"),
+            new_code_cell("cat(stringi::stri_rand_lipsum(3), sep='\n\n')"),
         ],
+        compare_ids=False,
     )
 
     markdown2 = jupytext.writes(nb, "md")
@@ -293,9 +254,10 @@ b = 2
 """,
 ):
     nb = jupytext.reads(markdown, "md")
-    compare(
-        nb.cells[0],
-        new_code_cell(source="a = 1\nb = 2", metadata={"tags": ["parameters"]}),
+    compare_cells(
+        nb.cells,
+        [new_code_cell(source="a = 1\nb = 2", metadata={"tags": ["parameters"]})],
+        compare_ids=False,
     )
     markdown2 = jupytext.writes(nb, "md")
     compare(markdown2, markdown)
@@ -308,7 +270,11 @@ raw content
 """,
 ):
     nb = jupytext.reads(markdown, "md")
-    compare(nb.cells[0], new_raw_cell(source="raw content", metadata={"key": "value"}))
+    compare_cells(
+        nb.cells,
+        [new_raw_cell(source="raw content", metadata={"key": "value"})],
+        compare_ids=False,
+    )
     markdown2 = jupytext.writes(nb, "md")
     compare(markdown2, markdown)
 
@@ -320,7 +286,11 @@ raw content
 """,
 ):
     nb = jupytext.reads(markdown, "md")
-    compare(nb.cells[0], new_raw_cell(source="raw content", metadata={"key": "value"}))
+    compare_cells(
+        nb.cells,
+        [new_raw_cell(source="raw content", metadata={"key": "value"})],
+        compare_ids=False,
+    )
     markdown2 = jupytext.writes(nb, "md")
     compare(markdown2, markdown)
 
@@ -342,7 +312,11 @@ raw content
 """,
 ):
     nb = jupytext.reads(markdown, "md")
-    compare(nb.cells[0], new_raw_cell(source="raw content", metadata={"key": "value"}))
+    compare_cells(
+        nb.cells,
+        [new_raw_cell(source="raw content", metadata={"key": "value"})],
+        compare_ids=False,
+    )
     md2 = jupytext.writes(nb, "md")
     assert "format_version: '1.1'" not in md2
 
@@ -376,14 +350,17 @@ jupyter:
 """,
 ):
     nb = jupytext.reads(header + "\n" + markdown_11, "md")
-    compare(
-        nb.cells[0],
-        new_raw_cell(
-            source=""".. meta::
+    compare_cells(
+        nb.cells,
+        [
+            new_raw_cell(
+                source=""".. meta::
    :description: Topic: Integrated Development Environments, Difficulty: Easy, Category: Tools
    :keywords: python, introduction, IDE, PyCharm, VSCode, Jupyter, recommendation, tools""",
-            metadata={"raw_mimetype": "text/restructuredtext"},
-        ),
+                metadata={"raw_mimetype": "text/restructuredtext"},
+            )
+        ],
+        compare_ids=False,
     )
     md2 = jupytext.writes(nb, "md")
     assert "format_version: '1.1'" not in md2
@@ -402,11 +379,14 @@ markdown cell
 """,
 ):
     nb = jupytext.reads(markdown, "md")
-    compare(
-        nb.cells[0],
-        new_markdown_cell(
-            source="A long\n\n\nmarkdown cell", metadata={"key": "value"}
-        ),
+    compare_cells(
+        nb.cells,
+        [
+            new_markdown_cell(
+                source="A long\n\n\nmarkdown cell", metadata={"key": "value"}
+            )
+        ],
+        compare_ids=False,
     )
     markdown2 = jupytext.writes(nb, "md")
     compare(markdown2, markdown)
@@ -422,11 +402,14 @@ markdown cell
 """,
 ):
     nb = jupytext.reads(markdown, "md")
-    compare(
-        nb.cells[0],
-        new_markdown_cell(
-            source="A long\n\n\nmarkdown cell", metadata={"key": "value"}
-        ),
+    compare_cells(
+        nb.cells,
+        [
+            new_markdown_cell(
+                source="A long\n\n\nmarkdown cell", metadata={"key": "value"}
+            )
+        ],
+        compare_ids=False,
     )
     markdown2 = jupytext.writes(nb, "md")
     compare(markdown2, markdown)
@@ -444,8 +427,14 @@ markdown cell
 """,
 ):
     nb = jupytext.reads(markdown, "md")
-    compare(nb.cells[0], new_markdown_cell(source="# A header"))
-    compare(nb.cells[1], new_markdown_cell(source="A long\n\n\nmarkdown cell"))
+    compare_cells(
+        nb.cells,
+        [
+            new_markdown_cell(source="# A header"),
+            new_markdown_cell(source="A long\n\n\nmarkdown cell"),
+        ],
+        compare_ids=False,
+    )
     markdown2 = jupytext.writes(nb, "md")
     compare(markdown2, markdown)
 
diff --git a/tests/test_read_simple_percent.py b/tests/test_read_simple_percent.py
index b971c33..85ba252 100644
--- a/tests/test_read_simple_percent.py
+++ b/tests/test_read_simple_percent.py
@@ -227,7 +227,7 @@ def test_multiple_empty_cells():
     compare(text, expected)
     nb2 = jupytext.reads(text, "py:percent")
     nb2.metadata = nb.metadata
-    compare(nb2, nb)
+    compare_notebooks(nb2, nb)
 
 
 def test_first_cell_markdown_191():
diff --git a/tests/test_read_simple_python.py b/tests/test_read_simple_python.py
index 229885e..4234901 100644
--- a/tests/test_read_simple_python.py
+++ b/tests/test_read_simple_python.py
@@ -858,7 +858,7 @@ def test_notebook_no_line_to_next_cell(
     nb2 = jupytext.reads(script, "py")
     nb2.metadata.pop("jupytext")
 
-    compare(nb2, nb)
+    compare_notebooks(nb2, nb)
 
 
 def test_notebook_one_blank_line_before_first_markdown_cell(
diff --git a/tests/test_read_simple_rmd.py b/tests/test_read_simple_rmd.py
index 23d218a..022246f 100644
--- a/tests/test_read_simple_rmd.py
+++ b/tests/test_read_simple_rmd.py
@@ -11,7 +11,7 @@ from nbformat.v4.nbbase import (
 
 import jupytext
 from jupytext.cli import jupytext as jupytext_cli
-from jupytext.compare import compare, compare_notebooks
+from jupytext.compare import compare, compare_cells, compare_notebooks
 
 
 def test_read_mostly_py_rmd_file(
@@ -39,43 +39,24 @@ cat(stringi::stri_rand_lipsum(3), sep='\n\n')
 """,
 ):
     nb = jupytext.reads(rmd, "Rmd")
-    compare(
+    compare_cells(
         nb.cells,
         [
-            {
-                "cell_type": "raw",
-                "source": "---\ntitle: Simple file\n---",
-                "metadata": {},
-            },
-            {
-                "cell_type": "code",
-                "metadata": {"echo": True},
-                "execution_count": None,
-                "source": "import numpy as np\n" "x = np.arange(0, 2*math.pi, eps)",
-                "outputs": [],
-            },
-            {
-                "cell_type": "code",
-                "metadata": {"echo": True},
-                "execution_count": None,
-                "source": "x = np.arange(0,1,eps)\ny = np.abs(x)-.5",
-                "outputs": [],
-            },
-            {
-                "cell_type": "code",
-                "metadata": {},
-                "execution_count": None,
-                "source": "%%R\nls()",
-                "outputs": [],
-            },
-            {
-                "cell_type": "code",
-                "metadata": {"results": "asis"},
-                "execution_count": None,
-                "source": "%%R -i x\ncat(stringi::" "stri_rand_lipsum(3), sep='\n\n')",
-                "outputs": [],
-            },
+            new_raw_cell("---\ntitle: Simple file\n---"),
+            new_code_cell(
+                "import numpy as np\n" "x = np.arange(0, 2*math.pi, eps)",
+                metadata={"echo": True},
+            ),
+            new_code_cell(
+                "x = np.arange(0,1,eps)\ny = np.abs(x)-.5", metadata={"echo": True}
+            ),
+            new_code_cell("%%R\nls()"),
+            new_code_cell(
+                "%%R -i x\ncat(stringi::" "stri_rand_lipsum(3), sep='\n\n')",
+                metadata={"results": "asis"},
+            ),
         ],
+        compare_ids=False,
     )
 
     rmd2 = jupytext.writes(nb, "Rmd")
@@ -194,20 +175,21 @@ title: R Markdown notebook title
     nb = nbformat.read(str(nb_file), as_version=4)
 
     if root_level_metadata_as_raw_cell:
-        assert len(nb.cells) == 2
-        compare(
-            nb.cells[0],
-            new_raw_cell(
-                """---
+        compare_cells(
+            nb.cells,
+            [
+                new_raw_cell(
+                    """---
 author: R Markdown document author
 title: R Markdown notebook title
 ---"""
-            ),
+                ),
+                new_code_cell("1 + 1"),
+            ],
+            compare_ids=False,
         )
-        compare(nb.cells[1], new_code_cell("1 + 1"))
     else:
-        assert len(nb.cells) == 1
-        compare(nb.cells[0], new_code_cell("1 + 1"))
+        compare_cells(nb.cells, [new_code_cell("1 + 1")], compare_ids=False)
         assert nb.metadata["jupytext"]["root_level_metadata"] == {
             "title": "R Markdown notebook title",
             "author": "R Markdown document author",
diff --git a/tests/test_read_write_functions.py b/tests/test_read_write_functions.py
index 54ece31..6f6a59b 100644
--- a/tests/test_read_write_functions.py
+++ b/tests/test_read_write_functions.py
@@ -5,7 +5,7 @@ import nbformat
 from nbformat.v4.nbbase import new_markdown_cell, new_notebook
 
 import jupytext
-from jupytext.compare import compare
+from jupytext.compare import compare, compare_notebooks
 
 
 def test_simple_hook(tmpdir):
@@ -81,4 +81,4 @@ def test_read_py_percent_from_stream():
 
     nb = jupytext.read(stream())
     nb2 = jupytext.read(stream(), fmt="py:percent")
-    compare(nb2, nb)
+    compare_notebooks(nb2, nb)