Repository: bskinn/sphobjinv

Issue:
Add trap for null subparser to avoid exception in CLI corner case where options are provided but subparser is not
~More idiomatic that way, and should provide a sensible `argparse` failed exit for things like `sphobjinv --foo`, instead of a mysterious exception deeper in the CLI code.~

~Annoying I had to [monkeypatch `argparse._VersionAction.__call__()`](https://stackoverflow.com/questions/3853722/how-to-insert-newlines-on-argparse-help-text#comment34784873_22157136) to have the newlines not get swallowed up, but what can you do?~

While `argparse` is not likely to change much over time, the above approach (see (tag)[https://github.com/bskinn/sphobjinv/tree/wontfix-required-subparser]) just really seemed too brittle.

Much better just to trap for a null subparser in the CLI code and cleanly error-exit.

Discussion:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index ff92ced..ccc2af3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -9,7 +9,7 @@ repos:
     -   id: check-yaml
     -   id: check-added-large-files
 -   repo: https://github.com/psf/black
-    rev: '22.1.0'
+    rev: '22.3.0'
     hooks:
     -   id: black
 -   repo: https://github.com/tox-dev/pyproject-fmt
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5752032..2932d15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,21 @@ and this project strives to adhere to
 [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 
 
+### (unreleased)
+
+#### Fixed
+
+  * CLI corner case where options are passed but no subparser is specified
+    now results in a clean error-exit, instead of an exception.
+    ([#239](https://github.com/bskinn/sphobjinv/issues/239))
+
+#### Internal
+
+  * Bump pre-commit black hook to v22.3.0
+
+  * Remove PyPy and Python 3.6 from Azure Pipelines test matrix
+
+
 ### [2.2.2] - 2022-03-22
 
 #### Fixed
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index da8b765..893d097 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -71,7 +71,7 @@ stages:
           spec: '3.10'
         pypy3:
           spec: 'pypy3'
-      platforms: [linux, windows]
+      platforms: [linux]
 
   - template: azure-coretest.yml
     parameters:
@@ -84,7 +84,7 @@ stages:
           spec: '3.9'
         py310:
           spec: '3.10'
-      platforms: [macOs]
+      platforms: [windows, macOs]
 
 
 - stage: aux_tests
@@ -203,5 +203,3 @@ stages:
 
     - script: tox -e flake8-noqa
       displayName: Run never-fail flake8 with noqa check
-
-
diff --git a/src/sphobjinv/cli/core.py b/src/sphobjinv/cli/core.py
index 731f1f1..a730335 100644
--- a/src/sphobjinv/cli/core.py
+++ b/src/sphobjinv/cli/core.py
@@ -190,6 +190,10 @@ def main():
         print(PrsConst.VER_TXT)
         sys.exit(0)
 
+    # At this point, need to trap for a null subparser
+    if not params[PrsConst.SUBPARSER_NAME]:
+        prs.error("No subparser selected")
+
     # Regardless of mode, insert extra blank line
     # for cosmetics
     log_print(" ", params)


Test patch:
diff --git a/conftest.py b/conftest.py
index 1ed15dd..b1c4096 100644
--- a/conftest.py
+++ b/conftest.py
@@ -218,7 +218,7 @@ def sphinx_version():
     """
     p_version = re.compile(r"(\d+)[.]?(\d+)?[.]?(\d+)?")
     mch = p_version.match(sphinx_version_str)
-    return tuple(map((lambda x: int(x) if x else 0), mch.groups()))
+    return tuple(int(x) if x else 0 for x in mch.groups())
 
 
 @pytest.fixture()  # Must be function scope since uses monkeypatch
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 145cc1a..99ba156 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -74,6 +74,22 @@ class TestMisc:
 
             assert "usage: sphobjinv" in out_.getvalue()
 
+    @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+    def test_cli_no_subparser_prs_exit(self, run_cmdline_test):
+        """Confirm exit code 2 if option passed but no subparser provided."""
+        with stdio_mgr() as (in_, out_, err_):
+            run_cmdline_test(["--foo"], expect=2)
+
+            assert "error: No subparser selected" in err_.getvalue()
+
+    @pytest.mark.timeout(CLI_TEST_TIMEOUT)
+    def test_cli_bad_subparser_prs_exit(self, run_cmdline_test):
+        """Confirm exit code 2 if invalid subparser provided."""
+        with stdio_mgr() as (in_, out_, err_):
+            run_cmdline_test(["foo"], expect=2)
+
+            assert "invalid choice: 'foo'" in err_.getvalue()
+
 
 class TestConvertGood:
     """Tests for expected-good convert functionality."""