Repository: mwouts/jupytext (language: python)

Issue:
Restore the coverage
The coverage is down by 3% at only 96% :)

This is probably caused by the update of the GitHub actions - for instance the tests that require a kernel don't seem to run anymore:
https://app.codecov.io/github/mwouts/jupytext/blob/main/tests%2Ftest_cli.py


Discussion / hints:

Fix patch:
diff --git a/src/jupytext/labconfig.py b/src/jupytext/labconfig.py
deleted file mode 100644
index 7c3d4fcf1..000000000
--- a/src/jupytext/labconfig.py
+++ /dev/null
@@ -1,119 +0,0 @@
-"""
-helper to inspect / initialize jupyterlab labconfig settings
-that are required to open jupytext notebooks in jupyterlab by default
-when these settings are not present, a double click on a jupytext
-notebook will cause jupyterlab to open it in an editor, i.e. as a text file
-"""
-
-import copy
-import json
-import logging
-
-# import pprint
-from pathlib import Path
-
-
-class LabConfig:
-    SETTINGS = Path.home() / ".jupyter" / "labconfig" / "default_setting_overrides.json"
-
-    DOCTYPES = [
-        "python",
-        "markdown",
-        "myst",
-        "r-markdown",
-        "quarto",
-        "julia",
-        "r",
-    ]
-
-    def __init__(self, logger=None):
-        self.logger = logger or logging.getLogger(__name__)
-        self.config = {}
-        # the state before any changes
-        self._prior_config = {}
-
-    def read(self):
-        """
-        read the labconfig settings file
-        """
-        try:
-            if self.SETTINGS.exists():
-                with self.SETTINGS.open() as fid:
-                    self.config = json.load(fid)
-        except OSError as exc:
-            self.logger.error(f"Could not read {self.SETTINGS}", exc)
-            return False
-        # store for further comparison
-        self._prior_config = copy.deepcopy(self.config)
-        return self
-
-    def list_default_viewer(self):
-        """
-        list the current labconfig settings
-        """
-        self.logger.debug(
-            f"Current @jupyterlab/docmanager-extension:plugin in {self.SETTINGS}"
-        )
-        docmanager = self.config.get("@jupyterlab/docmanager-extension:plugin", {})
-        viewers = docmanager.get("defaultViewers", {})
-        for key, value in viewers.items():
-            print(f"{key}: {value}")
-
-    def set_default_viewers(self, doctypes=None):
-        if not doctypes:
-            doctypes = self.DOCTYPES
-        for doctype in doctypes:
-            self.set_default_viewer(doctype)
-        return self
-
-    def set_default_viewer(self, doctype):
-        if "@jupyterlab/docmanager-extension:plugin" not in self.config:
-            self.config["@jupyterlab/docmanager-extension:plugin"] = {}
-        if (
-            "defaultViewers"
-            not in self.config["@jupyterlab/docmanager-extension:plugin"]
-        ):
-            self.config["@jupyterlab/docmanager-extension:plugin"][
-                "defaultViewers"
-            ] = {}
-        viewers = self.config["@jupyterlab/docmanager-extension:plugin"][
-            "defaultViewers"
-        ]
-        if doctype not in viewers:
-            viewers[doctype] = "Jupytext Notebook"
-
-    def unset_default_viewers(self, doctypes=None):
-        if not doctypes:
-            doctypes = self.DOCTYPES
-        for doctype in doctypes:
-            self.unset_default_viewer(doctype)
-        return self
-
-    def unset_default_viewer(self, doctype):
-        viewers = self.config.get("@jupyterlab/docmanager-extension:plugin", {}).get(
-            "defaultViewers", {}
-        )
-        if doctype not in viewers:
-            return
-        del viewers[doctype]
-
-    def write(self) -> bool:
-        """
-        write the labconfig settings file
-        """
-        # compare - avoid changing the file if nothing changed
-        if self.config == self._prior_config:
-            self.logger.info(f"Nothing to do for {self.SETTINGS}")
-            return True
-
-        # save
-        try:
-            self.SETTINGS.parent.mkdir(parents=True, exist_ok=True)
-            with self.SETTINGS.open("w") as fid:
-                json.dump(self.config, fid, indent=2)
-            # useful in case of successive write's
-            self._prior_config = copy.deepcopy(self.config)
-            return True
-        except OSError as exc:
-            self.logger.error(f"Could not write {self.SETTINGS}", exc)
-            return False
diff --git a/src/jupytext_config/__main__.py b/src/jupytext_config/__main__.py
index 5220474ac..a5538fe60 100644
--- a/src/jupytext_config/__main__.py
+++ b/src/jupytext_config/__main__.py
@@ -7,7 +7,7 @@
 
 import sys
 
-from jupytext.jupytext_config import main
+from .jupytext_config import main
 
 if __name__ == "__main__":
     sys.exit(main())
diff --git a/src/jupytext/jupytext_config.py b/src/jupytext_config/jupytext_config.py
similarity index 73%
rename from src/jupytext/jupytext_config.py
rename to src/jupytext_config/jupytext_config.py
index 25ca5464f..871395e33 100644
--- a/src/jupytext/jupytext_config.py
+++ b/src/jupytext_config/jupytext_config.py
@@ -6,6 +6,9 @@
 
 import sys
 from argparse import ArgumentParser
+from pathlib import Path
+
+import jupyter_core.paths as jupyter_core_paths
 
 from .labconfig import LabConfig
 
@@ -23,20 +26,15 @@ def main(self, args):
         """
         return 0 if all goes well
         """
-        print(
-            f"{self.__class__.__name__}: redefine main() to implement this subcommand"
-        )
-        return 1
+        raise NotImplementedError()  # pragma: no cover
 
 
 class ListDefaultViewer(SubCommand):
     def __init__(self):
-        super().__init__(
-            "list-default-viewer", "Display current settings in labconfig/"
-        )
+        super().__init__("list-default-viewer", "Display current settings in labconfig")
 
     def main(self, args):
-        LabConfig().read().list_default_viewer()
+        LabConfig(settings_file=args.settings_file).read().list_default_viewer()
         return 0
 
     def fill_parser(self, subparser):
@@ -48,7 +46,9 @@ def __init__(self):
         super().__init__("set-default-viewer", "Set default viewers for JupyterLab")
 
     def main(self, args):
-        LabConfig().read().set_default_viewers(args.doctype).write()
+        LabConfig(settings_file=args.settings_file).read().set_default_viewers(
+            args.doctype
+        ).write()
         return 0
 
     def fill_parser(self, subparser):
@@ -65,7 +65,9 @@ def __init__(self):
         super().__init__("unset-default-viewer", "Unset default viewers for JupyterLab")
 
     def main(self, args):
-        LabConfig().read().unset_default_viewers(args.doctype).write()
+        LabConfig(settings_file=args.settings_file).read().unset_default_viewers(
+            args.doctype
+        ).write()
         return 0
 
     def fill_parser(self, subparser):
@@ -87,6 +89,12 @@ def fill_parser(self, subparser):
 
 def main():
     parser = ArgumentParser()
+    parser.add_argument(
+        "--settings-file",
+        default=Path(jupyter_core_paths.jupyter_config_dir())
+        / "labconfig"
+        / "default_setting_overrides.json",
+    )
     subparsers = parser.add_subparsers(required=True)
     for subcommand in SUBCOMMANDS:
         subparser = subparsers.add_parser(subcommand.name, help=subcommand.help)
diff --git a/src/jupytext_config/labconfig.py b/src/jupytext_config/labconfig.py
new file mode 100644
index 000000000..05c3ac2da
--- /dev/null
+++ b/src/jupytext_config/labconfig.py
@@ -0,0 +1,86 @@
+"""
+helper to inspect / initialize jupyterlab labconfig settings
+that are required to open jupytext notebooks in jupyterlab by default
+when these settings are not present, a double click on a jupytext
+notebook will cause jupyterlab to open it in an editor, i.e. as a text file
+"""
+
+import json
+import logging
+from pathlib import Path
+
+
+class LabConfig:
+    DOCTYPES = [
+        "python",
+        "markdown",
+        "myst",
+        "r-markdown",
+        "quarto",
+        "julia",
+        "r",
+    ]
+
+    def __init__(self, *, settings_file, logger=None):
+        self.settings_file = Path(settings_file)
+        self.logger = logger or logging.getLogger(__name__)
+        self.config = {}
+
+    def read(self):
+        """
+        read the labconfig settings file
+        """
+        if self.settings_file.exists():
+            with self.settings_file.open() as fid:
+                self.config = json.load(fid)
+        else:
+            self.logger.info(f"Could not read from {self.settings_file} (not found)")
+
+        return self
+
+    def get_viewers(self):
+        return self.config.setdefault(
+            "@jupyterlab/docmanager-extension:plugin", {}
+        ).setdefault("defaultViewers", {})
+
+    def list_default_viewer(self):
+        """
+        list the current labconfig settings
+        """
+        self.logger.debug(
+            f"Current @jupyterlab/docmanager-extension:plugin in {self.settings_file}"
+        )
+        for key, value in self.get_viewers().items():
+            print(f"{key}: {value}")
+
+    def set_default_viewers(self, doctypes=None):
+        if not doctypes:
+            doctypes = self.DOCTYPES
+        for doctype in doctypes:
+            self.set_default_viewer(doctype)
+        return self
+
+    def set_default_viewer(self, doctype):
+        viewers = self.get_viewers()
+        if doctype not in viewers:
+            viewers[doctype] = "Jupytext Notebook"
+
+    def unset_default_viewers(self, doctypes=None):
+        if not doctypes:
+            doctypes = self.DOCTYPES
+        for doctype in doctypes:
+            self.unset_default_viewer(doctype)
+        return self
+
+    def unset_default_viewer(self, doctype):
+        viewers = self.get_viewers()
+        if doctype in viewers:
+            del viewers[doctype]
+
+    def write(self):
+        """
+        write the labconfig settings file
+        """
+        self.settings_file.parent.mkdir(parents=True, exist_ok=True)
+        with self.settings_file.open("w") as fid:
+            json.dump(self.config, fid, indent=2)


Test patch:
diff --git a/tests/integration/jupytext_config/test_jupytext_config.py b/tests/integration/jupytext_config/test_jupytext_config.py
new file mode 100644
index 000000000..9b54cc6ec
--- /dev/null
+++ b/tests/integration/jupytext_config/test_jupytext_config.py
@@ -0,0 +1,30 @@
+from jupytext.cli import system
+
+
+def test_jupytext_config_cli(tmp_path):
+    settings_file = tmp_path / "default_setting_overrides.json"
+    system("jupytext-config", "-h")
+    system(
+        "jupytext-config",
+        "--settings-file",
+        str(settings_file),
+        "set-default-viewer",
+        "python",
+        "markdown",
+    )
+    assert "python" in (settings_file).read_text()
+    assert "markdown" in system(
+        "jupytext-config", "--settings-file", settings_file, "list-default-viewer"
+    )
+    system(
+        "jupytext-config",
+        "--settings-file",
+        str(settings_file),
+        "unset-default-viewer",
+        "markdown",
+    )
+    list_viewers = system(
+        "jupytext-config", "--settings-file", settings_file, "list-default-viewer"
+    )
+    assert "python" in list_viewers
+    assert "markdown" not in list_viewers
diff --git a/tests/unit/test_labconfig.py b/tests/unit/test_labconfig.py
new file mode 100644
index 000000000..e96f9f326
--- /dev/null
+++ b/tests/unit/test_labconfig.py
@@ -0,0 +1,55 @@
+import json
+
+import pytest
+
+from jupytext_config.labconfig import LabConfig
+
+
+@pytest.fixture()
+def sample_viewer_config():
+    return {
+        "@jupyterlab/docmanager-extension:plugin": {
+            "defaultViewers": {
+                "markdown": "Jupytext Notebook",
+                "myst": "Jupytext Notebook",
+                "r-markdown": "Jupytext Notebook",
+                "quarto": "Jupytext Notebook",
+                "julia": "Jupytext Notebook",
+                "python": "Jupytext Notebook",
+                "r": "Jupytext Notebook",
+            }
+        }
+    }
+
+
+@pytest.fixture()
+def sample_empty_viewer_config():
+    return {"@jupyterlab/docmanager-extension:plugin": {"defaultViewers": {}}}
+
+
+@pytest.fixture()
+def settings_file(tmp_path):
+    return tmp_path / "default_setting_overrides.json"
+
+
+def test_read_config(settings_file, sample_viewer_config):
+    (settings_file).write_text(json.dumps(sample_viewer_config))
+    labconfig = LabConfig(settings_file=settings_file).read()
+    assert labconfig.config == sample_viewer_config
+
+
+def test_set_unset_default_viewers(
+    settings_file, sample_viewer_config, sample_empty_viewer_config
+):
+    labconfig = LabConfig(settings_file=settings_file)
+    labconfig.set_default_viewers()
+    assert labconfig.config == sample_viewer_config
+    labconfig.unset_default_viewers()
+    assert labconfig.config == sample_empty_viewer_config
+
+
+def test_write_config(settings_file, sample_viewer_config):
+    labconfig = LabConfig(settings_file=settings_file)
+    labconfig.set_default_viewers()
+    labconfig.write()
+    assert json.loads(settings_file.read_text()) == sample_viewer_config