Repository: mwouts/jupytext

Issue:
Jupytext manager seems to fail with jupyterfs
The content manager for jupytext seems to want a call _get_os_path which seems to break with jupyter-fs metamanager 
see  jpmorganchase/jupyter-fs#53


Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/mwouts/jupytext/pull/634?src=pr&el=h1) Report
> Merging [#634](https://codecov.io/gh/mwouts/jupytext/pull/634?src=pr&el=desc) into [master](https://codecov.io/gh/mwouts/jupytext/commit/aea84d9850fe24a71fe871064ffb0646f5a44eb4?el=desc) will **increase** coverage by `0.02%`.
> The diff coverage is `98.86%`.

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

```diff
@@            Coverage Diff             @@
##           master     #634      +/-   ##
==========================================
+ Coverage   99.18%   99.20%   +0.02%     
==========================================
  Files          94       94              
  Lines        9307     9333      +26     
==========================================
+ Hits         9231     9259      +28     
+ Misses         76       74       -2     
```


| [Impacted Files](https://codecov.io/gh/mwouts/jupytext/pull/634?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [jupytext/contentsmanager.py](https://codecov.io/gh/mwouts/jupytext/pull/634/diff?src=pr&el=tree#diff-anVweXRleHQvY29udGVudHNtYW5hZ2VyLnB5) | `98.50% <97.95%> (+0.05%)` | :arrow_up: |
| [tests/test\_cli.py](https://codecov.io/gh/mwouts/jupytext/pull/634/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9jbGkucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/test\_cm\_config.py](https://codecov.io/gh/mwouts/jupytext/pull/634/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9jbV9jb25maWcucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/test\_contentsmanager.py](https://codecov.io/gh/mwouts/jupytext/pull/634/diff?src=pr&el=tree#diff-dGVzdHMvdGVzdF9jb250ZW50c21hbmFnZXIucHk=) | `100.00% <100.00%> (ø)` | |
| [jupytext/formats.py](https://codecov.io/gh/mwouts/jupytext/pull/634/diff?src=pr&el=tree#diff-anVweXRleHQvZm9ybWF0cy5weQ==) | `98.74% <0.00%> (+0.31%)` | :arrow_up: |
| [jupytext/config.py](https://codecov.io/gh/mwouts/jupytext/pull/634/diff?src=pr&el=tree#diff-anVweXRleHQvY29uZmlnLnB5) | `97.51% <0.00%> (+0.62%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/mwouts/jupytext/pull/634?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/634?src=pr&el=footer). Last update [aea84d9...af2b15b](https://codecov.io/gh/mwouts/jupytext/pull/634?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

mwouts: With this version I see a warning: `'dict' object has no attribute 'metadata'` when I save paired notebooks.
telamonian: @mwouts I'll take a look
mwouts: Thank you @telamonian , well sure it is a great idea if you want to have a look at this. With this PR the contents manager will be much cleaner - the implementation in Jupytext <= 1.6.0 works well in practice but is a bit hacky!

The tests on this PR pass but as mentioned above the contents manager does not work in the real world. I am a bit surprised that the tests don't catch this... I'll see tonight if I can reproduce the problem in the test framework, and report here.
telamonian: @mwouts I tracked the problem down. All the details are at #635.

I'm trying to put together some tutorials for how to debug Jupyter-related code. So as an experiment/rough draft I filmed myself working on this bug and uploaded it to youtube.

- part 1: https://www.youtube.com/watch?v=VB-0o6JZVE4
  - tutorial and basic setup
- part 2: (still uploading)
  - applying the stuff form part 1 to solve the bug from https://github.com/mwouts/jupytext/pull/634#issuecomment-699834971, beginning to end

If you have a sec, check it out
joequant: Great job!!!!

Is there any place we can start a wiki to talk about debugging and testing techniques?

The tools that I use are Firefox developer and KernelSpy, and I find that a lot of bugs can be tracked down by just running these two programs and seeing where you have error messages, or weird behaviour.

Also, I have some hints as to guessing where the bugs often are.

Patch:
diff --git a/.gitignore b/.gitignore
index 1ba731b..17921bf 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,3 @@
-.idea
-.vscode
 .build
 .cache
 .coverage
@@ -18,3 +16,13 @@ docs/_build
 
 # Will be created by postBuild
 demo/get_started.ipynb
+
+# jetbrains ide stuff
+*.iml
+.idea/
+
+# vscode ide stuff
+*.code-workspace
+.history
+.vscode/*
+!.vscode/*.template
diff --git a/jupytext/contentsmanager.py b/jupytext/contentsmanager.py
index c29a5dc..2c59921 100644
--- a/jupytext/contentsmanager.py
+++ b/jupytext/contentsmanager.py
@@ -5,11 +5,6 @@ import itertools
 from datetime import timedelta, datetime
 from collections import namedtuple
 import nbformat
-
-try:
-    import unittest.mock as mock
-except ImportError:
-    import mock
 from tornado.web import HTTPError
 
 # import notebook.transutils before notebook.services.contents.filemanager #75
@@ -18,7 +13,7 @@ try:
 except ImportError:
     pass
 
-from .jupytext import reads, writes
+import jupytext
 from .formats import long_form_multiple_formats
 from .formats import short_form_one_format, short_form_multiple_formats
 from .paired_paths import (
@@ -42,20 +37,6 @@ from .config import (
 )
 
 
-def _jupytext_writes(fmt):
-    def _writes(nbk, version=nbformat.NO_CONVERT, **kwargs):
-        return writes(nbk, fmt, version=version, **kwargs)
-
-    return _writes
-
-
-def _jupytext_reads(fmt):
-    def _reads(text, as_version, **kwargs):
-        return reads(text, fmt, as_version=as_version, **kwargs)
-
-    return _reads
-
-
 def build_jupytext_contents_manager_class(base_contents_manager_class):
     """Derives a TextFileContentsManager class from the given base class"""
 
@@ -72,9 +53,11 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
             self.paired_notebooks = dict()
 
             # Configuration cache, useful when notebooks are listed in a given directory
-            self.cached_config = namedtuple("cached_config", "path timestamp config")
-
-            super(JupytextContentsManager, self).__init__(*args, **kwargs)
+            self.cached_config = namedtuple(
+                "cached_config", "path timestamp config_file config"
+            )
+            self.super = super(JupytextContentsManager, self)
+            self.super.__init__(*args, **kwargs)
 
         def all_nb_extensions(self):
             """All extensions that should be classified as notebooks"""
@@ -123,12 +106,12 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
                 if not self.dir_exists(parent_dir):
                     self.create_prefix_dir(parent_dir, fmt)
                     self.log.info("Creating directory %s", parent_dir)
-                    self.save(dict(type="directory"), parent_dir)
+                    self.super.save(dict(type="directory"), parent_dir)
 
         def save(self, model, path=""):
             """Save the file model and return the model with no content."""
             if model["type"] != "notebook":
-                return super(JupytextContentsManager, self).save(model, path)
+                return self.super.save(model, path)
 
             path = path.strip("/")
             nbk = model["content"]
@@ -154,7 +137,7 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
 
                     self.create_prefix_dir(path, fmt)
                     if fmt["extension"] == ".ipynb":
-                        return super(JupytextContentsManager, self).save(model, path)
+                        return self.super.save(model, path)
 
                     if (
                         model["content"]["metadata"]
@@ -169,13 +152,25 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
                             )
                         )
 
-                    with mock.patch("nbformat.writes", _jupytext_writes(fmt)):
-                        return super(JupytextContentsManager, self).save(model, path)
+                    text_model = dict(
+                        type="file",
+                        format="text",
+                        content=jupytext.writes(
+                            nbformat.from_dict(model["content"]), fmt=fmt
+                        ),
+                    )
+
+                    return self.super.save(text_model, path)
 
                 return write_pair(path, jupytext_formats, save_one_file)
 
-            except Exception as err:
-                raise HTTPError(400, str(err))
+            except Exception as e:
+                self.log.error(
+                    u"Error while saving file: %s %s", path, e, exc_info=True
+                )
+                raise HTTPError(
+                    500, u"Unexpected error while saving file: %s %s" % (path, e)
+                )
 
         def get(
             self,
@@ -195,18 +190,21 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
                 or self.dir_exists(path)
                 or (type != "notebook" if type else ext not in self.all_nb_extensions())
             ):
-                return super(JupytextContentsManager, self).get(
-                    path, content, type, format
-                )
+                return self.super.get(path, content, type, format)
 
             config = self.get_config(path, use_cache=content is False)
             fmt = preferred_format(ext, config.preferred_jupytext_formats_read)
             if ext == ".ipynb":
-                model = self._notebook_model(path, content=content)
+                model = self.super.get(path, content, type="notebook", format=format)
             else:
+                model = self.super.get(path, content, type="file", format=format)
+                model["type"] = "notebook"
                 config.set_default_format_options(fmt, read=True)
-                with mock.patch("nbformat.reads", _jupytext_reads(fmt)):
-                    model = self._notebook_model(path, content=content)
+                if content:
+                    try:
+                        model["content"] = jupytext.reads(model["content"], fmt=fmt)
+                    except Exception as err:
+                        raise HTTPError(400, str(err))
 
             if not load_alternative_format:
                 return model
@@ -220,7 +218,7 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
                 fmt, formats = self.paired_notebooks.get(path)
                 for alt_path, _ in paired_paths(path, fmt, formats):
                     if alt_path != path and self.exists(alt_path):
-                        alt_model = self._notebook_model(alt_path, content=False)
+                        alt_model = self.super.get(alt_path, content=False)
                         if alt_model["last_modified"] > model["last_modified"]:
                             model["last_modified"] = alt_model["last_modified"]
 
@@ -262,19 +260,23 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
                     return None
                 if alt_path == path:
                     return model["last_modified"]
-                return self._notebook_model(alt_path, content=False)["last_modified"]
+                return self.super.get(alt_path, content=False)["last_modified"]
 
             def read_one_file(alt_path, alt_fmt):
                 if alt_path == path:
                     return model["content"]
                 if alt_path.endswith(".ipynb"):
                     self.log.info(u"Reading OUTPUTS from {}".format(alt_path))
-                    return self._notebook_model(alt_path, content=True)["content"]
+                    return self.super.get(
+                        alt_path, content=True, type="notebook", format=format
+                    )["content"]
 
                 self.log.info(u"Reading SOURCE from {}".format(alt_path))
                 config.set_default_format_options(alt_fmt, read=True)
-                with mock.patch("nbformat.reads", _jupytext_reads(alt_fmt)):
-                    return self._notebook_model(alt_path, content=True)["content"]
+                text = self.super.get(
+                    alt_path, content=True, type="file", format=format
+                )["content"]
+                return jupytext.reads(text, fmt=alt_fmt)
 
             inputs, outputs = latest_inputs_and_outputs(
                 path, fmt, formats, get_timestamp, contents_manager_mode=True
@@ -335,9 +337,7 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
             into account when type=="notebook". See https://github.com/mwouts/jupytext/issues/443
             """
             if type != "notebook" and ext != ".ipynb":
-                return super(JupytextContentsManager, self).new_untitled(
-                    path, type, ext
-                )
+                return self.super.new_untitled(path, type, ext)
 
             ext = ext or ".ipynb"
             if ":" in ext:
@@ -386,13 +386,13 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
         def trust_notebook(self, path):
             """Trust the current notebook"""
             if path.endswith(".ipynb") or path not in self.paired_notebooks:
-                super(JupytextContentsManager, self).trust_notebook(path)
+                self.super.trust_notebook(path)
                 return
 
             fmt, formats = self.paired_notebooks[path]
             for alt_path, alt_fmt in paired_paths(path, fmt, formats):
                 if alt_fmt["extension"] == ".ipynb":
-                    super(JupytextContentsManager, self).trust_notebook(alt_path)
+                    self.super.trust_notebook(alt_path)
 
         def rename_file(self, old_path, new_path):
             """Rename the current notebook, as well as its alternative representations"""
@@ -405,7 +405,7 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
                     pass
 
             if old_path not in self.paired_notebooks:
-                super(JupytextContentsManager, self).rename_file(old_path, new_path)
+                self.super.rename_file(old_path, new_path)
                 return
 
             fmt, formats = self.paired_notebooks.get(old_path)
@@ -420,9 +420,7 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
             for old_alt_path, alt_fmt in old_alt_paths:
                 new_alt_path = full_path(new_base, alt_fmt)
                 if self.exists(old_alt_path):
-                    super(JupytextContentsManager, self).rename_file(
-                        old_alt_path, new_alt_path
-                    )
+                    self.super.rename_file(old_alt_path, new_alt_path)
 
             self.drop_paired_notebook(old_path)
             self.update_paired_notebooks(new_path, formats)
@@ -431,6 +429,11 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
             """The parent directory"""
             if "/" in path:
                 return path.rsplit("/", 1)[0]
+            # jupyter-fs
+            if ":" in path and hasattr(self, "_managers"):
+                if path.endswith(":"):
+                    return ""
+                return path.rsplit(":", 1)[0] + ":"
             return ""
 
         def get_config_file(self, directory):
@@ -441,22 +444,23 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
                     return path
 
             if not directory:
-                return find_global_jupytext_configuration_file()
+                return None
 
             parent_dir = self.get_parent_dir(directory)
             return self.get_config_file(parent_dir)
 
-        def load_config_file(self, config_file):
+        def load_config_file(self, config_file, is_os_path=False):
             """Load the configuration file"""
             if config_file is None:
                 return None
             self.log.info("Loading Jupytext configuration file at %s", config_file)
-            if config_file.endswith(".py"):
-                config_dict = load_jupytext_configuration_file(
-                    self._get_os_path(config_file)
-                )
+            if config_file.endswith(".py") and not is_os_path:
+                config_file = self._get_os_path(config_file)
+                is_os_path = True
+            if is_os_path:
+                config_dict = load_jupytext_configuration_file(config_file)
             else:
-                model = self.get(config_file, content=True, type="file")
+                model = self.super.get(config_file, content=True, type="file")
                 config_dict = load_jupytext_configuration_file(
                     config_file, model["content"]
                 )
@@ -477,13 +481,27 @@ def build_jupytext_contents_manager_class(base_contents_manager_class):
             ):
                 try:
                     config_file = self.get_config_file(parent_dir)
-                    self.cached_config.config = self.load_config_file(config_file)
+                    if config_file:
+                        self.cached_config.config = self.load_config_file(config_file)
+                    else:
+                        config_file = find_global_jupytext_configuration_file()
+                        self.cached_config.config = self.load_config_file(
+                            config_file, True
+                        )
+                    self.cached_config.config_file = config_file
                     self.cached_config.path = parent_dir
                     self.cached_config.timestamp = datetime.now()
                 except JupytextConfigurationError as err:
                     raise HTTPError(400, "{}".format(err))
 
-            return self.cached_config.config or self
+            if self.cached_config.config is not None:
+                self.log.debug(
+                    "Configuration file for %s is %s",
+                    path,
+                    self.cached_config.config_file,
+                )
+                return self.cached_config.config
+            return self
 
     return JupytextContentsManager
 
diff --git a/setup.cfg b/setup.cfg
index e769ae8..848f32c 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -18,4 +18,4 @@ exclude_lines =
 [flake8]
 max-line-length=127
 exclude=tests/notebooks/*
-ignore=E203,W503
+ignore=E203,E231,W503


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 167fb5e..9fe6410 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -1166,3 +1166,6 @@ def test_jupytext_set_formats_file_gives_an_informative_error(tmpdir):
 
     with pytest.raises(ValueError, match="jupytext --sync notebook.md"):
         jupytext(["--set-formats", "notebook.md"])
+
+    # Remove the config file, otherwise test_jupytext_jupyter_fs_metamanager fails later on!
+    cfg_file.remove()
diff --git a/tests/test_cm_config.py b/tests/test_cm_config.py
index 06a0c20..168c886 100644
--- a/tests/test_cm_config.py
+++ b/tests/test_cm_config.py
@@ -9,6 +9,7 @@ from nbformat import read
 import pytest
 from tornado.web import HTTPError
 import jupytext
+from .utils import notebook_model
 
 SAMPLE_NOTEBOOK = new_notebook(
     cells=[new_markdown_cell("A Markdown cell"), new_code_cell("# A code cell\n1 + 1")]
@@ -24,11 +25,11 @@ def test_local_config_overrides_cm_config(tmpdir):
     with open(str(nested.join(".jupytext.yml")), "w") as fp:
         fp.write("default_jupytext_formats: ''\n")
 
-    cm.save(dict(type="notebook", content=SAMPLE_NOTEBOOK), "notebook.ipynb")
+    cm.save(notebook_model(SAMPLE_NOTEBOOK), "notebook.ipynb")
     assert os.path.isfile(str(tmpdir.join("notebook.ipynb")))
     assert os.path.isfile(str(tmpdir.join("notebook.py")))
 
-    cm.save(dict(type="notebook", content=SAMPLE_NOTEBOOK), "nested/notebook.ipynb")
+    cm.save(notebook_model(SAMPLE_NOTEBOOK), "nested/notebook.ipynb")
     assert os.path.isfile(str(nested.join("notebook.ipynb")))
     assert not os.path.isfile(str(nested.join("notebook.py")))
 
@@ -64,7 +65,7 @@ def test_pairing_through_config_leaves_ipynb_unmodified(tmpdir):
 
     cfg_file.write("default_jupytext_formats: 'ipynb,py'\n")
 
-    cm.save(dict(type="notebook", content=SAMPLE_NOTEBOOK), "notebook.ipynb")
+    cm.save(notebook_model(SAMPLE_NOTEBOOK), "notebook.ipynb")
     assert nb_file.isfile()
     assert py_file.isfile()
 
@@ -99,4 +100,27 @@ def test_incorrect_config_message(tmpdir, cfg_file, cfg_text):
         cm.get("empty.ipynb", type="notebook", content=False)
 
     with pytest.raises(HTTPError, match=expected_message):
-        cm.save(dict(type="notebook", content=SAMPLE_NOTEBOOK), "notebook.ipynb")
+        cm.save(notebook_model(SAMPLE_NOTEBOOK), "notebook.ipynb")
+
+
+def test_global_config_file(tmpdir):
+    cm_dir = tmpdir.join("cm_dir").mkdir()
+    cm = jupytext.TextFileContentsManager()
+    cm.root_dir = str(cm_dir)
+
+    tmpdir.join("jupytext.toml").write('default_jupytext_formats = "ipynb,Rmd"')
+
+    def fake_global_config_directory():
+        return [str(tmpdir)]
+
+    with mock.patch(
+        "jupytext.config.global_jupytext_configuration_directories",
+        fake_global_config_directory,
+    ):
+        nb = new_notebook(cells=[new_code_cell("1+1")])
+        model = notebook_model(nb)
+        cm.save(model, "notebook.ipynb")
+        assert set(model["path"] for model in cm.get("/", content=True)["content"]) == {
+            "notebook.ipynb",
+            "notebook.Rmd",
+        }
diff --git a/tests/test_contentsmanager.py b/tests/test_contentsmanager.py
index 24fda0c..c14c9d6 100644
--- a/tests/test_contentsmanager.py
+++ b/tests/test_contentsmanager.py
@@ -5,6 +5,7 @@ import re
 import time
 import pytest
 import itertools
+import logging
 import shutil
 from nbformat.v4.nbbase import new_notebook, new_markdown_cell, new_code_cell
 from tornado.web import HTTPError
@@ -21,6 +22,7 @@ from .utils import (
     requires_sphinx_gallery,
     requires_pandoc,
     skip_if_dict_is_not_ordered,
+    notebook_model,
 )
 
 
@@ -90,12 +92,12 @@ def test_pair_unpair_notebook(tmpdir):
     cm.root_dir = str(tmpdir)
 
     # save notebook
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
     assert not os.path.isfile(str(tmpdir.join(tmp_md)))
 
     # pair notebook
     nb["metadata"]["jupytext"] = {"formats": "ipynb,md"}
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
     assert os.path.isfile(str(tmpdir.join(tmp_md)))
 
     # reload and get outputs
@@ -104,7 +106,7 @@ def test_pair_unpair_notebook(tmpdir):
 
     # unpair and save as md
     del nb["metadata"]["jupytext"]
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_md)
+    cm.save(model=notebook_model(nb), path=tmp_md)
     nb2 = cm.get(tmp_md)["content"]
 
     # we get no outputs here
@@ -124,12 +126,12 @@ def test_load_save_rename(nb_file, tmpdir):
 
     # open ipynb, save Rmd, reopen
     nb = jupytext.read(nb_file)
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_rmd)
+    cm.save(model=notebook_model(nb), path=tmp_rmd)
     nb_rmd = cm.get(tmp_rmd)
     compare_notebooks(nb_rmd["content"], nb, "Rmd")
 
     # save ipynb
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # rename ipynb
     cm.rename(tmp_ipynb, "new.ipynb")
@@ -144,7 +146,7 @@ def test_load_save_rename(nb_file, tmpdir):
     assert not os.path.isfile(str(tmpdir.join("new.Rmd")))
     model = cm.get("new.ipynb", content=False)
     assert "last_modified" in model
-    cm.save(model=dict(type="notebook", content=nb), path="new.ipynb")
+    cm.save(model=notebook_model(nb), path="new.ipynb")
     assert os.path.isfile(str(tmpdir.join("new.Rmd")))
 
     cm.delete("new.Rmd")
@@ -169,7 +171,7 @@ def test_save_load_paired_md_notebook(nb_file, tmpdir):
     nb = jupytext.read(nb_file)
     nb.metadata["jupytext"] = {"formats": "ipynb,md"}
 
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
     nb_md = cm.get(tmp_md)
 
     compare_notebooks(nb_md["content"], nb, "md")
@@ -193,7 +195,7 @@ def test_save_load_paired_md_pandoc_notebook(nb_file, tmpdir):
     nb = jupytext.read(nb_file)
     nb.metadata["jupytext"] = {"formats": "ipynb,md:pandoc"}
 
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
     nb_md = cm.get(tmp_md)
 
     compare_notebooks(nb_md["content"], nb, "md:pandoc")
@@ -212,7 +214,7 @@ def test_pair_plain_script(py_file, tmpdir, caplog):
     # open py file, pair, save with cm
     nb = jupytext.read(py_file)
     nb.metadata["jupytext"]["formats"] = "ipynb,py:hydrogen"
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_py)
+    cm.save(model=notebook_model(nb), path=tmp_py)
 
     assert "'Include Metadata' is off" in caplog.text
 
@@ -235,7 +237,7 @@ def test_pair_plain_script(py_file, tmpdir, caplog):
 
     # remove the pairing and save
     del nb.metadata["jupytext"]["formats"]
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_py)
+    cm.save(model=notebook_model(nb), path=tmp_py)
 
     # reopen py file with the cm
     nb2 = cm.get(tmp_py)["content"]
@@ -255,12 +257,12 @@ def test_load_save_rename_nbpy(nb_file, tmpdir):
 
     # open ipynb, save nb.py, reopen
     nb = jupytext.read(nb_file)
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_nbpy)
+    cm.save(model=notebook_model(nb), path=tmp_nbpy)
     nbpy = cm.get(tmp_nbpy)
     compare_notebooks(nbpy["content"], nb)
 
     # save ipynb
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # rename nbpy
     cm.rename(tmp_nbpy, "new.nb.py")
@@ -293,7 +295,7 @@ def test_load_save_py_freeze_metadata(script, tmpdir):
 
     # open and save notebook
     nb = cm.get(tmp_nbpy)["content"]
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_nbpy)
+    cm.save(model=notebook_model(nb), path=tmp_nbpy)
 
     with open(str(tmpdir.join(tmp_nbpy))) as fp:
         text_py2 = fp.read()
@@ -313,12 +315,12 @@ def test_load_save_rename_notebook_with_dot(nb_file, tmpdir):
 
     # open ipynb, save nb.py, reopen
     nb = jupytext.read(nb_file)
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_nbpy)
+    cm.save(model=notebook_model(nb), path=tmp_nbpy)
     nbpy = cm.get(tmp_nbpy)
     compare_notebooks(nbpy["content"], nb)
 
     # save ipynb
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # rename py
     cm.rename(tmp_nbpy, "2.new_notebook.py")
@@ -342,7 +344,7 @@ def test_load_save_rename_nbpy_default_config(nb_file, tmpdir):
     # open ipynb, save nb.py, reopen
     nb = jupytext.read(nb_file)
 
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_nbpy)
+    cm.save(model=notebook_model(nb), path=tmp_nbpy)
     nbpy = cm.get(tmp_nbpy)
     compare_notebooks(nbpy["content"], nb)
 
@@ -351,7 +353,7 @@ def test_load_save_rename_nbpy_default_config(nb_file, tmpdir):
     compare_notebooks(nbipynb["content"], nb)
 
     # save ipynb
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # rename notebook.nb.py to new.nb.py
     cm.rename(tmp_nbpy, "new.nb.py")
@@ -383,7 +385,7 @@ def test_load_save_rename_non_ascii_path(nb_file, tmpdir):
     # open ipynb, save nb.py, reopen
     nb = jupytext.read(nb_file)
 
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_nbpy)
+    cm.save(model=notebook_model(nb), path=tmp_nbpy)
     nbpy = cm.get(tmp_nbpy)
     compare_notebooks(nbpy["content"], nb)
 
@@ -392,7 +394,7 @@ def test_load_save_rename_non_ascii_path(nb_file, tmpdir):
     compare_notebooks(nbipynb["content"], nb)
 
     # save ipynb
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # rename notebôk.nb.py to nêw.nb.py
     cm.rename(tmp_nbpy, u"nêw.nb.py")
@@ -424,7 +426,7 @@ def test_outdated_text_notebook(nb_file, tmpdir):
 
     # open ipynb, save py, reopen
     nb = jupytext.read(nb_file)
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_nbpy)
+    cm.save(model=notebook_model(nb), path=tmp_nbpy)
     model_py = cm.get(tmp_nbpy, load_alternative_format=False)
     model_ipynb = cm.get(tmp_ipynb, load_alternative_format=False)
 
@@ -463,7 +465,7 @@ def test_reload_notebook_after_jupytext_cli(nb_file, tmpdir):
     # write the paired notebook
     nb = jupytext.read(nb_file)
     nb.metadata.setdefault("jupytext", {})["formats"] = "py,ipynb"
-    cm.save(model=dict(type="notebook", content=nb), path="notebook.py")
+    cm.save(model=notebook_model(nb), path="notebook.py")
 
     assert os.path.isfile(tmp_ipynb)
     assert os.path.isfile(tmp_nbpy)
@@ -494,7 +496,7 @@ def test_load_save_percent_format(nb_file, tmpdir):
     # open python, save
     nb = cm.get(tmp_py)["content"]
     del nb.metadata["jupytext"]["notebook_metadata_filter"]
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_py)
+    cm.save(model=notebook_model(nb), path=tmp_py)
 
     # compare the new file with original one
     with open(str(tmpdir.join(tmp_py))) as stream:
@@ -524,7 +526,7 @@ def test_save_to_percent_format(nb_file, tmpdir):
     nb["metadata"]["jupytext"] = {"formats": "ipynb,jl"}
 
     # save to ipynb and jl
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # read jl file
     with open(str(tmpdir.join(tmp_jl))) as stream:
@@ -549,7 +551,7 @@ def test_save_using_preferred_and_default_format_170(nb_file, tmpdir):
     cm.default_jupytext_formats = "ipynb,python//py"
 
     # save to ipynb and py
-    cm.save(model=dict(type="notebook", content=nb), path="notebook.ipynb")
+    cm.save(model=notebook_model(nb), path="notebook.ipynb")
 
     # read py file
     nb_py = read(tmp_py)
@@ -564,7 +566,7 @@ def test_save_using_preferred_and_default_format_170(nb_file, tmpdir):
     cm.default_jupytext_formats = "ipynb,python//py"
 
     # save to ipynb and py
-    cm.save(model=dict(type="notebook", content=nb), path="notebook.ipynb")
+    cm.save(model=notebook_model(nb), path="notebook.ipynb")
 
     # read py file
     nb_py = read(tmp_py)
@@ -578,7 +580,7 @@ def test_save_using_preferred_and_default_format_170(nb_file, tmpdir):
     cm.default_jupytext_formats = "ipynb,python//py:percent"
 
     # save to ipynb and py
-    cm.save(model=dict(type="notebook", content=nb), path="notebook.ipynb")
+    cm.save(model=notebook_model(nb), path="notebook.ipynb")
 
     # read py file
     nb_py = read(tmp_py)
@@ -668,7 +670,7 @@ def test_save_to_light_percent_sphinx_format(nb_file, tmpdir):
     }
 
     # save to ipynb and three python flavors
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # read files
     with open(str(tmpdir.join(tmp_pct_py))) as stream:
@@ -707,7 +709,7 @@ def test_pair_notebook_with_dot(nb_file, tmpdir):
     nb["metadata"]["jupytext"] = {"formats": "ipynb,py:percent"}
 
     # save to ipynb and three python flavors
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     assert os.path.isfile(str(tmpdir.join(tmp_ipynb)))
 
@@ -737,7 +739,7 @@ def test_preferred_format_allows_to_read_others_format(nb_file, tmpdir):
     # load notebook and save it using the cm
     nb = jupytext.read(nb_file)
     nb["metadata"]["jupytext"] = {"formats": "ipynb,py"}
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # Saving does not update the metadata, as 'save' makes a copy of the notebook
     # assert nb['metadata']['jupytext']['formats'] == 'ipynb,py:light'
@@ -757,7 +759,7 @@ def test_preferred_format_allows_to_read_others_format(nb_file, tmpdir):
     # Change save format and save
     model["content"]["metadata"]["jupytext"]["formats"] == "ipynb,py"
     cm.preferred_jupytext_formats_save = "py:percent"
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # Read notebook
     model = cm.get(tmp_nbpy)
@@ -809,7 +811,7 @@ def test_save_in_auto_extension_global(nb_file, tmpdir):
     cm.root_dir = str(tmpdir)
 
     # save notebook
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # check that text representation exists, and is in percent format
     with open(str(tmpdir.join(tmp_script))) as stream:
@@ -837,7 +839,7 @@ def test_global_auto_pairing_works_with_empty_notebook(tmpdir):
     cm.root_dir = str(tmpdir)
 
     # save notebook
-    cm.save(model=dict(type="notebook", content=nb), path="notebook.ipynb")
+    cm.save(model=notebook_model(nb), path="notebook.ipynb")
 
     # check that only the ipynb representation exists
     assert os.path.isfile(tmp_ipynb)
@@ -860,7 +862,7 @@ def test_global_auto_pairing_works_with_empty_notebook(tmpdir):
     }
 
     # save again
-    cm.save(model=dict(type="notebook", content=nb), path="notebook.ipynb")
+    cm.save(model=notebook_model(nb), path="notebook.ipynb")
 
     # check that ipynb + py representations exists
     assert os.path.isfile(tmp_ipynb)
@@ -892,7 +894,7 @@ def test_save_in_auto_extension_global_with_format(nb_file, tmpdir):
     cm.root_dir = str(tmpdir)
 
     # save notebook
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # check that text representation exists, and is in percent format
     with open(str(tmpdir.join(tmp_script))) as stream:
@@ -922,7 +924,7 @@ def test_save_in_auto_extension_local(nb_file, tmpdir):
     cm.root_dir = str(tmpdir)
 
     # save notebook
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # check that text representation exists, and is in percent format
     with open(str(tmpdir.join(tmp_script))) as stream:
@@ -951,7 +953,7 @@ def test_save_in_pct_and_lgt_auto_extensions(nb_file, tmpdir):
     cm.root_dir = str(tmpdir)
 
     # save notebook
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # check that text representation exists in percent format
     with open(str(tmpdir.join(tmp_pct_script))) as stream:
@@ -973,7 +975,7 @@ def test_metadata_filter_is_effective(nb_file, tmpdir):
     cm.root_dir = str(tmpdir)
 
     # save notebook to tmpdir
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # set config
     cm.default_jupytext_formats = "ipynb,py"
@@ -987,7 +989,7 @@ def test_metadata_filter_is_effective(nb_file, tmpdir):
     assert nb.metadata["jupytext"]["notebook_metadata_filter"] == "jupytext,-all"
 
     # save notebook again
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     # read text version
     nb2 = jupytext.read(str(tmpdir.join(tmp_script)))
@@ -1062,7 +1064,7 @@ def test_local_format_can_deactivate_pairing(nb_file, ext, tmpdir):
     cm.root_dir = str(tmpdir)
 
     # save notebook
-    cm.save(model=dict(type="notebook", content=nb), path="notebook" + ext)
+    cm.save(model=notebook_model(nb), path="notebook" + ext)
 
     # check that only the text representation exists
     assert os.path.isfile(str(tmpdir.join("notebook.py"))) == (ext == ".py")
@@ -1071,7 +1073,7 @@ def test_local_format_can_deactivate_pairing(nb_file, ext, tmpdir):
     compare_notebooks(nb2, nb)
 
     # resave, check again
-    cm.save(model=dict(type="notebook", content=nb2), path="notebook" + ext)
+    cm.save(model=notebook_model(nb2), path="notebook" + ext)
 
     assert os.path.isfile(str(tmpdir.join("notebook.py"))) == (ext == ".py")
     assert os.path.isfile(str(tmpdir.join("notebook.ipynb"))) == (ext == ".ipynb")
@@ -1090,7 +1092,7 @@ def test_global_pairing_allows_to_save_other_file_types(nb_file, tmpdir):
     cm.root_dir = str(tmpdir)
 
     # save notebook
-    cm.save(model=dict(type="notebook", content=nb), path="notebook.Rmd")
+    cm.save(model=notebook_model(nb), path="notebook.Rmd")
 
     # check that only the original file is saved
     assert os.path.isfile(str(tmpdir.join("notebook.Rmd")))
@@ -1135,9 +1137,8 @@ def test_pair_notebook_in_another_folder(tmpdir):
     tmp_py = str(tmpdir.join("scripts/notebook_name.py"))
 
     cm.save(
-        model=dict(
-            type="notebook",
-            content=new_notebook(
+        model=notebook_model(
+            new_notebook(
                 metadata={"jupytext": {"formats": "notebooks//ipynb,scripts//py"}}
             ),
         ),
@@ -1160,11 +1161,8 @@ def test_pair_notebook_in_dotdot_folder(tmpdir):
     tmp_py = str(tmpdir.join("scripts/notebook_name.py"))
 
     cm.save(
-        model=dict(
-            type="notebook",
-            content=new_notebook(
-                metadata={"jupytext": {"formats": "ipynb,../scripts//py"}}
-            ),
+        model=notebook_model(
+            new_notebook(metadata={"jupytext": {"formats": "ipynb,../scripts//py"}})
         ),
         path="notebooks/notebook_name.ipynb",
     )
@@ -1268,19 +1266,19 @@ def test_set_then_change_formats(tmpdir):
     cm = jupytext.TextFileContentsManager()
     cm.root_dir = str(tmpdir)
 
-    cm.save(model=dict(content=nb, type="notebook"), path="nb.ipynb")
+    cm.save(model=notebook_model(nb), path="nb.ipynb")
     assert os.path.isfile(tmp_py)
     assert read(tmp_py).metadata["jupytext"]["formats"] == "ipynb,py:light"
     os.remove(tmp_py)
 
     nb.metadata["jupytext"]["formats"] = "ipynb,py:percent"
-    cm.save(model=dict(content=nb, type="notebook"), path="nb.ipynb")
+    cm.save(model=notebook_model(nb), path="nb.ipynb")
     assert os.path.isfile(tmp_py)
     assert read(tmp_py).metadata["jupytext"]["formats"] == "ipynb,py:percent"
     os.remove(tmp_py)
 
     del nb.metadata["jupytext"]["formats"]
-    cm.save(model=dict(content=nb, type="notebook"), path="nb.ipynb")
+    cm.save(model=notebook_model(nb), path="nb.ipynb")
     assert not os.path.isfile(tmp_py)
 
 
@@ -1296,7 +1294,7 @@ def test_set_then_change_auto_formats(tmpdir, nb_file):
 
     # Pair ipynb/py and save
     nb.metadata["jupytext"] = {"formats": "ipynb,auto:light"}
-    cm.save(model=dict(content=nb, type="notebook"), path="nb.ipynb")
+    cm.save(model=notebook_model(nb), path="nb.ipynb")
     assert "nb.py" in cm.paired_notebooks
     assert "nb.auto" not in cm.paired_notebooks
     assert os.path.isfile(tmp_py)
@@ -1305,7 +1303,7 @@ def test_set_then_change_auto_formats(tmpdir, nb_file):
     # Pair ipynb/Rmd and save
     time.sleep(0.5)
     nb.metadata["jupytext"] = {"formats": "ipynb,Rmd"}
-    cm.save(model=dict(content=nb, type="notebook"), path="nb.ipynb")
+    cm.save(model=notebook_model(nb), path="nb.ipynb")
     assert "nb.Rmd" in cm.paired_notebooks
     assert "nb.py" not in cm.paired_notebooks
     assert "nb.auto" not in cm.paired_notebooks
@@ -1316,7 +1314,7 @@ def test_set_then_change_auto_formats(tmpdir, nb_file):
     # Unpair and save
     time.sleep(0.5)
     del nb.metadata["jupytext"]
-    cm.save(model=dict(content=nb, type="notebook"), path="nb.ipynb")
+    cm.save(model=notebook_model(nb), path="nb.ipynb")
     assert "nb.Rmd" not in cm.paired_notebooks
     assert "nb.py" not in cm.paired_notebooks
     assert "nb.auto" not in cm.paired_notebooks
@@ -1342,7 +1340,7 @@ def test_share_py_recreate_ipynb(tmpdir, nb_file):
     cm.default_cell_metadata_filter = "-all"
 
     nb = read(nb_file)
-    model_ipynb = cm.save(model=dict(content=nb, type="notebook"), path="nb.ipynb")
+    model_ipynb = cm.save(model=notebook_model(nb), path="nb.ipynb")
 
     assert os.path.isfile(tmp_ipynb)
     assert os.path.isfile(tmp_py)
@@ -1382,7 +1380,7 @@ def test_vim_folding_markers(tmpdir):
             new_code_cell("a = 1\n\n\nb = 1"),
         ]
     )
-    cm.save(model=dict(content=nb, type="notebook"), path="nb.ipynb")
+    cm.save(model=notebook_model(nb), path="nb.ipynb")
 
     assert os.path.isfile(tmp_ipynb)
     assert os.path.isfile(tmp_py)
@@ -1438,7 +1436,7 @@ def test_vscode_pycharm_folding_markers(tmpdir):
             new_code_cell("a = 1\n\n\nb = 1"),
         ]
     )
-    cm.save(model=dict(content=nb, type="notebook"), path="nb.ipynb")
+    cm.save(model=notebook_model(nb), path="nb.ipynb")
 
     assert os.path.isfile(tmp_ipynb)
     assert os.path.isfile(tmp_py)
@@ -1494,7 +1492,7 @@ def test_open_file_with_default_cell_markers(tmpdir):
     nb = cm.get("nb.py")["content"]
     assert len(nb.cells) == 1
 
-    cm.save(model=dict(type="notebook", content=nb), path="nb.py")
+    cm.save(model=notebook_model(nb), path="nb.py")
 
     with open(tmp_py) as fp:
         text2 = fp.read()
@@ -1534,7 +1532,7 @@ def test_save_file_with_default_cell_markers(tmpdir):
 
     nb.metadata["jupytext"]["cell_markers"] = "+,-"
     del nb.metadata["jupytext"]["notebook_metadata_filter"]
-    cm.save(model=dict(type="notebook", content=nb), path="nb.py")
+    cm.save(model=notebook_model(nb), path="nb.py")
 
     with open(tmp_py) as fp:
         text2 = fp.read()
@@ -1634,9 +1632,7 @@ def test_multiple_pairing(tmpdir):
     cm = jupytext.TextFileContentsManager()
     cm.root_dir = str(tmpdir)
 
-    cm.save(
-        model=dict(type="notebook", content=nb("saved from cm")), path="notebook.ipynb"
-    )
+    cm.save(model=notebook_model(nb("saved from cm")), path="notebook.ipynb")
     compare_notebooks(jupytext.read(tmp_ipynb), nb("saved from cm"))
     compare_notebooks(jupytext.read(tmp_md), nb("saved from cm"))
     compare_notebooks(jupytext.read(tmp_py), nb("saved from cm"))
@@ -1689,7 +1685,7 @@ def test_filter_jupytext_version_information_416(nb_file, tmpdir):
     # load notebook
     notebook = jupytext.read(nb_file)
     notebook.metadata["jupytext_formats"] = "ipynb,py"
-    model = dict(type="notebook", content=notebook)
+    model = notebook_model(notebook)
 
     # save to ipynb and py
     cm.save(model=model, path="notebook.ipynb")
@@ -1726,34 +1722,46 @@ def test_nested_prefix(tmpdir):
         cells=[new_code_cell("1+1"), new_markdown_cell("Some text")],
         metadata={"jupytext": {"formats": "ipynb,nested/prefix//.py"}},
     )
-    cm.save(model=dict(type="notebook", content=nb), path="notebook.ipynb")
+    cm.save(model=notebook_model(nb), path="notebook.ipynb")
 
     assert tmpdir.join("nested").join("prefix").join("notebook.py").isfile()
 
 
-def test_jupytext_jupyter_fs_manager(tmpdir):
-    """Test the basic get/save functions of Jupytext with a fs manager
-    https://github.com/mwouts/jupytext/issues/618"""
+def fs_meta_manager(tmpdir):
     try:
-        from jupyterfs.fsmanager import FSManager
+        from jupyterfs.metamanager import MetaManager
     except ImportError:
         pytest.skip("jupyterfs is not available")
 
-    cm_class = jupytext.build_jupytext_contents_manager_class(FSManager)
-    cm = cm_class("osfs://{local_dir}".format(local_dir=tmpdir))
-    cm.default_jupytext_formats = ""
+    cm_class = jupytext.build_jupytext_contents_manager_class(MetaManager)
+    logger = logging.getLogger("jupyter-fs")
+    cm = cm_class(parent=None, log=logger)
+    cm.initResource(
+        {
+            "url": "osfs://{}".format(tmpdir),
+        }
+    )
+    return cm
+
+
+def test_jupytext_jupyter_fs_metamanager(tmpdir):
+    """Test the basic get/save functions of Jupytext with a fs manager
+    https://github.com/mwouts/jupytext/issues/618"""
+    cm = fs_meta_manager(tmpdir)
+    # the hash that corresponds to the osfs
+    osfs = [h for h in cm._managers if h != ""][0]
 
     # save a few files
     text = "some text\n"
-    cm.save(dict(type="file", content=text, format="text"), path="text.md")
+    cm.save(dict(type="file", content=text, format="text"), path=osfs + ":text.md")
     nb = new_notebook(
         cells=[new_markdown_cell("A markdown cell"), new_code_cell("1 + 1")]
     )
-    cm.save(dict(type="notebook", content=nb), "notebook.ipynb")
-    cm.save(dict(type="notebook", content=nb), "text_notebook.md")
+    cm.save(notebook_model(nb), osfs + ":notebook.ipynb")
+    cm.save(notebook_model(nb), osfs + ":text_notebook.md")
 
     # list the directory
-    directory = cm.get("/")
+    directory = cm.get(osfs + ":/")
     assert set(file["name"] for file in directory["content"]) == {
         "text.md",
         "text_notebook.md",
@@ -1761,17 +1769,17 @@ def test_jupytext_jupyter_fs_manager(tmpdir):
     }
 
     # get the files
-    model = cm.get("/text.md", type="file")
+    model = cm.get(osfs + ":/text.md", type="file")
     assert model["type"] == "file"
     assert model["content"] == text
 
-    model = cm.get("/text.md", type="notebook")
+    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())])
 
     for nb_file in ["notebook.ipynb", "text_notebook.md"]:
-        model = cm.get(nb_file)
+        model = cm.get(osfs + ":" + nb_file)
         assert model["type"] == "notebook"
         actual_cells = model["content"].cells
 
@@ -1781,28 +1789,22 @@ def test_jupytext_jupyter_fs_manager(tmpdir):
         compare(actual_cells, nb.cells)
 
 
-def test_config_jupytext_jupyter_fs_manager(tmpdir):
+def test_config_jupytext_jupyter_fs_meta_manager(tmpdir):
     """Test the configuration of Jupytext with a fs manager"""
-    try:
-        from jupyterfs.fsmanager import FSManager
-    except ImportError:
-        pytest.skip("jupyterfs is not available")
-
     tmpdir.join("jupytext.toml").write('default_jupytext_formats = "ipynb,py"')
-
-    cm_class = jupytext.build_jupytext_contents_manager_class(FSManager)
-    cm = cm_class("osfs://{local_dir}".format(local_dir=tmpdir))
-    cm.default_jupytext_formats = ""
+    cm = fs_meta_manager(tmpdir)
+    # the hash that corresponds to the osfs
+    osfs = [h for h in cm._managers if h != ""][0]
 
     # save a few files
     nb = new_notebook()
-    cm.save(dict(type="file", content="text", format="text"), path="text.md")
-    cm.save(dict(type="notebook", content=nb), "script.py")
-    cm.save(dict(type="notebook", content=nb), "text_notebook.md")
-    cm.save(dict(type="notebook", content=nb), "notebook.ipynb")
+    cm.save(dict(type="file", content="text", format="text"), path=osfs + ":text.md")
+    cm.save(notebook_model(nb), osfs + ":script.py")
+    cm.save(notebook_model(nb), osfs + ":text_notebook.md")
+    cm.save(notebook_model(nb), osfs + ":notebook.ipynb")
 
     # list the directory
-    directory = cm.get("/")
+    directory = cm.get(osfs + ":/")
     assert set(file["name"] for file in directory["content"]) == {
         "jupytext.toml",
         "text.md",
diff --git a/tests/test_escape_magics.py b/tests/test_escape_magics.py
index 108e2db..b483128 100644
--- a/tests/test_escape_magics.py
+++ b/tests/test_escape_magics.py
@@ -4,6 +4,7 @@ from jupytext.compare import compare
 from jupytext.magics import comment_magic, uncomment_magic, unesc, is_magic
 from jupytext.compare import compare_notebooks
 import jupytext
+from .utils import notebook_model
 
 
 def test_unesc():
@@ -144,12 +145,12 @@ def test_force_comment_using_contents_manager(tmpdir):
 
     nb = new_notebook(cells=[new_code_cell("%pylab inline")])
 
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_py)
+    cm.save(model=notebook_model(nb), path=tmp_py)
     with open(str(tmpdir.join(tmp_py))) as stream:
         assert "# %pylab inline" in stream.read().splitlines()
 
     cm.comment_magics = False
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_py)
+    cm.save(model=notebook_model(nb), path=tmp_py)
     with open(str(tmpdir.join(tmp_py))) as stream:
         assert "%pylab inline" in stream.read().splitlines()
 
diff --git a/tests/test_read_simple_percent.py b/tests/test_read_simple_percent.py
index bfc79ff..1dfe319 100644
--- a/tests/test_read_simple_percent.py
+++ b/tests/test_read_simple_percent.py
@@ -10,6 +10,7 @@ from nbformat.v4.nbbase import (
 )
 from jupytext.compare import compare, compare_notebooks
 import jupytext
+from .utils import notebook_model
 
 
 def test_read_simple_file(
@@ -352,7 +353,7 @@ def test_cell_markers_option_in_contents_manager(tmpdir):
             }
         },
     )
-    cm.save(model=dict(type="notebook", content=nb), path="notebook.ipynb")
+    cm.save(model=notebook_model(nb), path="notebook.ipynb")
 
     assert os.path.isfile(tmp_ipynb)
     assert os.path.isfile(tmp_py)
@@ -395,7 +396,7 @@ def test_default_cell_markers_in_contents_manager(tmpdir):
             }
         },
     )
-    cm.save(model=dict(type="notebook", content=nb), path="notebook.ipynb")
+    cm.save(model=notebook_model(nb), path="notebook.ipynb")
 
     assert os.path.isfile(tmp_ipynb)
     assert os.path.isfile(tmp_py)
@@ -436,7 +437,7 @@ def test_default_cell_markers_in_contents_manager_does_not_impact_light_format(t
         },
     )
     with pytest.warns(UserWarning, match="Ignored cell markers"):
-        cm.save(model=dict(type="notebook", content=nb), path="notebook.ipynb")
+        cm.save(model=notebook_model(nb), path="notebook.ipynb")
 
     assert os.path.isfile(tmp_ipynb)
     assert os.path.isfile(tmp_py)
diff --git a/tests/test_save_multiple.py b/tests/test_save_multiple.py
index a378c9a..0c6c119 100644
--- a/tests/test_save_multiple.py
+++ b/tests/test_save_multiple.py
@@ -6,7 +6,7 @@ from tornado.web import HTTPError
 import jupytext
 from jupytext.contentsmanager import TextFileContentsManager
 from jupytext.compare import compare_notebooks
-from .utils import list_notebooks
+from .utils import list_notebooks, notebook_model
 
 
 @pytest.mark.parametrize("nb_file", list_notebooks(skip="66"))
@@ -20,7 +20,7 @@ def test_rmd_is_ok(nb_file, tmpdir):
     cm = TextFileContentsManager()
     cm.root_dir = str(tmpdir)
 
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     nb2 = jupytext.read(str(tmpdir.join(tmp_rmd)))
 
@@ -37,7 +37,7 @@ def test_ipynb_is_ok(nb_file, tmpdir):
     cm.root_dir = str(tmpdir)
     cm.default_jupytext_formats = "ipynb,Rmd"
 
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_rmd)
+    cm.save(model=notebook_model(nb), path=tmp_rmd)
 
     nb2 = jupytext.read(str(tmpdir.join(tmp_ipynb)))
     compare_notebooks(nb2, nb)
@@ -54,7 +54,7 @@ def test_all_files_created(nb_file, tmpdir):
     cm = TextFileContentsManager()
     cm.root_dir = str(tmpdir)
 
-    cm.save(model=dict(type="notebook", content=nb), path=tmp_ipynb)
+    cm.save(model=notebook_model(nb), path=tmp_ipynb)
 
     nb2 = jupytext.read(str(tmpdir.join(tmp_py)))
     compare_notebooks(nb2, nb)
@@ -73,7 +73,7 @@ def test_no_files_created_on_no_format(tmpdir):
     cm.default_jupytext_formats = ""
 
     cm.save(
-        model=dict(type="notebook", content=new_notebook(nbformat=4, metadata=dict())),
+        model=notebook_model(new_notebook(nbformat=4, metadata=dict())),
         path=tmp_ipynb,
     )
 
@@ -121,8 +121,6 @@ def test_no_rmd_on_not_v4(tmpdir):
     cm.default_jupytext_formats = "ipynb,Rmd"
 
     with pytest.raises(NotebookValidationError):
-        cm.save(
-            model=dict(type="notebook", content=new_notebook(nbformat=3)), path=tmp_rmd
-        )
+        cm.save(model=notebook_model(new_notebook(nbformat=3)), path=tmp_rmd)
 
     assert not os.path.isfile(str(tmpdir.join(tmp_ipynb)))
diff --git a/tests/test_trust_notebook.py b/tests/test_trust_notebook.py
index 8f12c4a..576e304 100644
--- a/tests/test_trust_notebook.py
+++ b/tests/test_trust_notebook.py
@@ -115,3 +115,31 @@ def test_ipynb_notebooks_can_be_trusted_even_with_metadata_filter(
         assert cell.metadata.get("trusted", True)
 
     compare_notebooks(nb2["content"], model["content"])
+
+
+@pytest.mark.parametrize("nb_file", list_notebooks("percent", skip="hash sign"))
+def test_text_notebooks_can_be_trusted(nb_file, tmpdir, no_jupytext_version_number):
+    cm = TextFileContentsManager()
+    root, file = os.path.split(nb_file)
+    py_file = str(tmpdir.join(file))
+    shutil.copy(nb_file, py_file)
+
+    cm.root_dir = str(tmpdir)
+    model = cm.get(file)
+    model["type"] == "notebook"
+    cm.save(model, file)
+
+    # Unsign notebook
+    nb = model["content"]
+    for cell in nb.cells:
+        if "trusted" in cell.metadata:
+            cell.metadata.pop("trusted")
+
+    cm.notary.unsign(nb)
+
+    # Trust and reload
+    cm.trust_notebook(file)
+
+    model = cm.get(file)
+    for cell in model["content"].cells:
+        assert cell.metadata.get("trusted", True)
diff --git a/tests/utils.py b/tests/utils.py
index 456f980..3837f97 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,6 +1,7 @@
 import os
 import sys
 import re
+import json
 import pytest
 from jupytext.cli import system
 from jupytext.cell_reader import rst2md
@@ -107,3 +108,8 @@ def list_notebooks(path="ipynb", skip="World"):
 
     # ignore ".ipynb_checkpoints" sub-folder
     return [nb_file for nb_file in notebooks if os.path.isfile(nb_file)]
+
+
+def notebook_model(nb):
+    """Return a notebook model, with content a dictionary rather than a notebook object"""
+    return dict(type="notebook", content=json.loads(json.dumps(nb)))