Repository: iterative/dvc

Issue:
shell completion issue with dvc 3.11.0 release build
# Bug Report

👋 while trying to upgrade dvc to the latest release, I ran into the shell completion failure

```
Successfully installed dvc-3.11.0
[31mERROR[39m: unexpected error - unsupported operand type(s) for %: 'tuple' and 'dict'

[33mHaving any troubles?[0m Hit us up at [34mhttps://dvc.org/support[0m, we are always happy to help!
[31mError:[0m Failure while executing; `\{\"SHELL\"=\>\"zsh\"\} /opt/homebrew/Cellar/dvc/3.11.0/bin/dvc completion -s zsh` exited with 255. Here's the output:
```

relates to https://github.com/Homebrew/homebrew-core/pull/138416


Discussion:
Thanks @chenrui333 for the report. This is broken due to https://github.com/iterative/dvc/pull/9760. Will fix shortly.

Resolving patch:
diff --git a/dvc/commands/commit.py b/dvc/commands/commit.py
--- a/dvc/commands/commit.py
+++ b/dvc/commands/commit.py
@@ -48,8 +48,8 @@ def add_parser(subparsers, parent_parser):
         action="store_true",
         default=False,
         help=(
-            "Commit data even if hash values for dependencies or",
-            "outputs did not change.",
+            "Commit data even if hash values for dependencies or "
+            "outputs did not change."
         ),
     )
     commit_parser.add_argument(
diff --git a/dvc/commands/completion.py b/dvc/commands/completion.py
--- a/dvc/commands/completion.py
+++ b/dvc/commands/completion.py
@@ -9,6 +9,9 @@
 logger = logging.getLogger(__name__)
 
 
+SUPPORTED_SHELLS = ["bash", "zsh"]
+
+
 class CmdCompletion(CmdBaseNoRepo):
     def run(self):
         import shtab
@@ -35,6 +38,6 @@ def add_parser(subparsers, parent_parser):
         "--shell",
         help="Shell syntax for completions.",
         default="bash",
-        choices=["bash", "zsh"],
+        choices=SUPPORTED_SHELLS,
     )
     completion_parser.set_defaults(func=CmdCompletion)


Test patch:
diff --git a/tests/unit/command/test_completion.py b/tests/unit/command/test_completion.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/command/test_completion.py
@@ -0,0 +1,17 @@
+import logging
+
+import pytest
+
+from dvc.cli import main
+from dvc.commands.completion import SUPPORTED_SHELLS
+
+
+@pytest.mark.parametrize("shell", SUPPORTED_SHELLS)
+def test_completion(caplog, capsys, shell):
+    with caplog.at_level(logging.INFO):
+        assert main(["completion", "-s", shell]) == 0
+    assert not caplog.text
+
+    out, err = capsys.readouterr()
+    assert not err
+    assert out
diff --git a/tests/unit/command/test_help.py b/tests/unit/command/test_help.py
new file mode 100644
--- /dev/null
+++ b/tests/unit/command/test_help.py
@@ -0,0 +1,48 @@
+import logging
+from argparse import SUPPRESS, ArgumentParser
+from typing import Tuple
+
+import pytest
+import shtab
+
+from dvc.cli import main
+from dvc.cli.parser import get_main_parser
+
+
+def command_tuples():
+    root: Tuple[str, ...] = ()
+    commands = [root]
+
+    def recurse_parser(parser: ArgumentParser, parents: Tuple[str, ...] = root) -> None:
+        for positional in parser._get_positional_actions():
+            if positional.help != SUPPRESS and isinstance(positional.choices, dict):
+                public_cmds = shtab.get_public_subcommands(positional)
+                for subcmd, subparser in positional.choices.items():
+                    cmd = (*parents, subcmd)
+                    if subcmd in public_cmds:
+                        commands.append(cmd)
+                    recurse_parser(subparser, cmd)
+
+    main_parser = get_main_parser()
+    recurse_parser(main_parser)
+    return sorted(commands)
+
+
+def pytest_generate_tests(metafunc):
+    def ids(values):
+        if values:
+            return "-".join(values)
+        return "dvc"
+
+    metafunc.parametrize("command_tuples", command_tuples(), ids=ids)
+
+
+def test_help(caplog, capsys, command_tuples):
+    with caplog.at_level(logging.INFO), pytest.raises(SystemExit) as e:
+        main([*command_tuples, "--help"])
+    assert e.value.code == 0
+    assert not caplog.text
+
+    out, err = capsys.readouterr()
+    assert not err
+    assert out