Repository: deathbeds/importnb

Issue:
add a `--version` switch to the CLI

Discussion:
bollwyvl: We might not even need a short flag for `--version`. 

Often `-v` is reserved for `--verbose`, and might be nice to reserve for future work. 

I would intend to use this downstream to catch packaging issues with a "black box" invocation like:

```
importnb --version | grep -iE "importnb {{ version.replace('.', '\.') }}$"
```

Related to the as-of-yet untriaged `[extras]` item from #146: since `pip` check doesn't care about extras (last i checked), perhaps `--version` (or `--versions`) could also print out all of the versions of the packages it _would_ use, if asked:

```
                importnb        2023.11.3

available optional dependencies:

    [doit]      doit            0.36.0
    [ipython]   IPython         8.17.2
    [toml]      tomli           2.0.1
    [yaml]      ruamel.yaml     0.18.5

missing optional dependencies:

    no missing optional dependencies:

```

With a "fail" condition like:

```
missing optional dependencies:

    [yaml]      To enable importnb.YAML:

                pip install importnb[yaml] 
```


tonyfast: i'll think about adding the extra dependency version in another PR i think.
bollwyvl: Pushed using the the built-in `action="version"`, and re-ran the linters...
bollwyvl: ![image](https://github.com/deathbeds/importnb/assets/45380/da650852-e113-4944-b269-d581cbc73464)

tonyfast: Hell yea! Thanks for adding ruff. Was afraid to open that box and see how bad the formatting might be. 
bollwyvl: Welp it's definitely just the "safely"-fixable stuff... I'm sure it would
have some complaints if given full agency...


Patch:
diff --git a/src/importnb/__init__.py b/src/importnb/__init__.py
index cfd6f3d..54d648d 100644
--- a/src/importnb/__init__.py
+++ b/src/importnb/__init__.py
@@ -27,7 +27,7 @@ def get_ipython(force=True):
 import builtins
 
 from ._version import __version__
-from .loader import Notebook, reload
 from .entry_points import imports
+from .loader import Notebook, reload
 
 builtins.true, builtins.false, builtins.null = True, False, None
diff --git a/src/importnb/docstrings.py b/src/importnb/docstrings.py
index 6161a9b..1fd45e9 100644
--- a/src/importnb/docstrings.py
+++ b/src/importnb/docstrings.py
@@ -26,7 +26,6 @@ str_nodes = (ast.Constant,)
 
 
 class TestStrings(ast.NodeTransformer):
-
     strings = None
 
     def visit_Module(self, module):
diff --git a/src/importnb/entry_points.py b/src/importnb/entry_points.py
index 75a4387..396593a 100644
--- a/src/importnb/entry_points.py
+++ b/src/importnb/entry_points.py
@@ -1,5 +1,5 @@
 import sys
-from contextlib import contextmanager, ExitStack
+from contextlib import ExitStack, contextmanager
 
 # See compatibility note on `group`
 # https://docs.python.org/3/library/importlib.metadata.html#entry-points
diff --git a/src/importnb/loader.py b/src/importnb/loader.py
index 459191f..699bbe8 100644
--- a/src/importnb/loader.py
+++ b/src/importnb/loader.py
@@ -316,7 +316,8 @@ class Loader(Interface, SourceFileLoader):
         if isinstance(argv, str):
             argv = shlex.split(argv)
 
-        module = cls.load_ns(parser.parse_args(argv))
+        parsed_args = parser.parse_args(argv)
+        module = cls.load_ns(parsed_args)
         if module is None:
             return parser.print_help()
 
@@ -370,6 +371,8 @@ class Loader(Interface, SourceFileLoader):
     def get_argparser(parser=None):
         from argparse import REMAINDER, ArgumentParser
 
+        from importnb import __version__
+
         if parser is None:
             parser = ArgumentParser("importnb", description="run notebooks as python code")
         parser.add_argument("file", nargs="?", help="run a file")
@@ -378,6 +381,9 @@ class Loader(Interface, SourceFileLoader):
         parser.add_argument("-c", "--code", help="run raw code")
         parser.add_argument("-d", "--dir", help="path to run script in")
         parser.add_argument("-t", "--tasks", action="store_true", help="run doit tasks")
+        parser.add_argument(
+            "--version", action="version", version=__version__, help="display the importnb version"
+        )
         return parser
 
 
diff --git a/src/importnb/loaders.py b/src/importnb/loaders.py
index e38e1a4..4c62637 100644
--- a/src/importnb/loaders.py
+++ b/src/importnb/loaders.py
@@ -1,7 +1,8 @@
-from .loader import Loader, SourceModule
 from dataclasses import dataclass, field
 from types import ModuleType
 
+from .loader import Loader, SourceModule
+
 
 class DataModule(SourceModule):
     def _repr_json_(self):


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index e779935..5b2b535 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -5,8 +5,38 @@ on:
     branches: ["*"]
   push:
     branches: [main]
+  workflow_dispatch:
+
+concurrency:
+  group: ${{ github.workflow }}-${{ github.ref }}
+  cancel-in-progress: true
 
 jobs:
+  format:
+    name: format
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v3
+      with:
+        fetch-depth: 0
+    - uses: actions/setup-python@v4
+      with:
+        python-version: "3.12"
+        cache: pip
+        cache-dependency-path: pyproject.toml
+    - name: install dev dependencies
+      run: python -m pip install --upgrade pip hatch
+    - name: run formatters
+      run: |
+        echo "~~~bash" > "${GITHUB_STEP_SUMMARY}"
+        hatch run format:code 2>&1 | tee --append "${GITHUB_STEP_SUMMARY}"
+        echo "~~~" >> "${GITHUB_STEP_SUMMARY}"
+    - name: print diff
+      run: |
+        echo "~~~diff" >> "${GITHUB_STEP_SUMMARY}"
+        git diff | tee --append "${GITHUB_STEP_SUMMARY}"
+        echo "~~~" >> "${GITHUB_STEP_SUMMARY}"
+
   test:
     name: ${{ matrix.os }} ${{ matrix.python-version }}
     runs-on: ${{ matrix.os }}
diff --git a/docs/test_cli.py b/docs/test_cli.py
index 3b94b72..86f10b9 100644
--- a/docs/test_cli.py
+++ b/docs/test_cli.py
@@ -5,6 +5,7 @@ from sys import executable, path, version_info
 from pytest import importorskip
 
 from importnb import Notebook
+from importnb import __version__ as importnb_version
 
 GTE10 = version_info.major == 3 and version_info.minor >= 10
 
@@ -34,7 +35,9 @@ def cli_test(command):
                 check_call([executable] + split(command), stderr=file, stdout=file)
             out = path.read_text()
             match = get_prepared_string(
-                f.__doc__.format(UNTITLED=UNTITLED.as_posix(), SLUG=ref.magic_slug)
+                f.__doc__.format(
+                    UNTITLED=UNTITLED.as_posix(), SLUG=ref.magic_slug, VERSION=importnb_version
+                )
             )
 
             if "UserWarning: Attempting to work in a virtualenv." in out:
@@ -49,7 +52,8 @@ def cli_test(command):
 @cli_test("-m importnb")
 def test_usage():
     """\
-usage: importnb [-h] [-m MODULE] [-c CODE] [-d DIR] [-t] [file] ...
+usage: importnb [-h] [-m MODULE] [-c CODE] [-d DIR] [-t] [--version]
+                [file] ...
 
 run notebooks as python code
 
@@ -64,6 +68,7 @@ optional arguments:
   -c CODE, --code CODE  run raw code
   -d DIR, --dir DIR     path to run script in
   -t, --tasks           run doit tasks
+  --version             display the importnb version
 """
 
 
@@ -90,6 +95,13 @@ def test_empty_code():
     """"""
 
 
+@cli_test("-m importnb --version")
+def test_version():
+    """\
+{VERSION}
+"""
+
+
 @cli_test(rf"-m importnb -d {UNTITLED.parent.as_posix()} -t {UNTITLED.as_posix()} list")
 def test_doit():
     """\
diff --git a/docs/test_importnb.py b/docs/test_importnb.py
index a0be83a..abb2030 100644
--- a/docs/test_importnb.py
+++ b/docs/test_importnb.py
@@ -215,7 +215,6 @@ def test_fuzzy_finder_conflict(clean, ref):
 
 
 def test_minified_json(ref, minified):
-
     with Notebook():
         import minified as minned
 
@@ -295,8 +294,12 @@ def test_top_level_async():
 def test_data_loaders(pytester):
     some_random_data = {"top": [{}]}
 
-    import json, tomli_w, io
+    import io
+    import json
+
+    import tomli_w
     from ruamel.yaml import YAML
+
     yaml = YAML(typ="safe", pure=True)
 
     sys.path.insert(0, str(pytester._path))
@@ -307,7 +310,9 @@ def test_data_loaders(pytester):
     pytester.makefile(".yaml", yaml_data=y.getvalue())
 
     with imports("json", "yaml", "toml"):
-        import json_data, yaml_data, toml_data
+        import json_data
+        import toml_data
+        import yaml_data
     assert json_data.__file__.endswith(".json")
     assert toml_data.__file__.endswith(".toml")
     assert yaml_data.__file__.endswith(".yaml")