Repository: omni-us/jsonargparse
Pull request #693 (2025-03-12 06:24:48)

shtab support literal and fix positionals
## Before submitting

- [x] Did you read the [contributing guideline](https://github.com/omni-us/jsonargparse/blob/main/CONTRIBUTING.rst)?
- [n/a] Did you update **the documentation**? (readme and public docstrings)
- [x] Did you write **unit tests** such that there is 100% coverage on related code? (required for bug fixes and new features)
- [x] Did you verify that new and existing **tests pass locally**?
- [x] Did you make sure that all changes preserve **backward compatibility**?
- [x] Did you update **the CHANGELOG**? (not for typos, docs, test updates, or minor internal changes/refactors)


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/omni-us/jsonargparse/pull/693?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) Report
Attention: Patch coverage is `87.50000%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 99.98%. Comparing base [(`745eac8`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/745eac8ea88bbcb26af7efb4f351ffa2a8e72fdc?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) to head [(`df68636`)](https://app.codecov.io/gh/omni-us/jsonargparse/commit/df686366532e4c05933987716cc880877e28707c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).

:white_check_mark: All tests successful. No failed tests found.

| [Files with missing lines](https://app.codecov.io/gh/omni-us/jsonargparse/pull/693?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) | Patch % | Lines |
|---|---|---|
| [jsonargparse/\_completions.py](https://app.codecov.io/gh/omni-us/jsonargparse/pull/693?src=pr&el=tree&filepath=jsonargparse%2F_completions.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us#diff-anNvbmFyZ3BhcnNlL19jb21wbGV0aW9ucy5weQ==) | 87.50% | [1 Missing :warning: ](https://app.codecov.io/gh/omni-us/jsonargparse/pull/693?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us) |

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff             @@
##              main     #693      +/-   ##
===========================================
- Coverage   100.00%   99.98%   -0.02%     
===========================================
  Files           22       22              
  Lines         6688     6693       +5     
===========================================
+ Hits          6688     6692       +4     
- Misses           0        1       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/omni-us/jsonargparse/pull/693?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=omni-us).

sonarqubecloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=693) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=693&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=693&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=693&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [87.5% Coverage on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=693&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=693&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=693)


sonarqubecloud[bot]: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=693) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=693&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=omni-us_jsonargparse&pullRequest=693&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=omni-us_jsonargparse&pullRequest=693&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=693&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=omni-us_jsonargparse&pullRequest=693&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=omni-us_jsonargparse&pullRequest=693)



Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 0ca5658..bdf5ac7 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -15,6 +15,11 @@ paths are considered internals and can change in minor and patch releases.
 v4.38.0 (2025-02-??)
 --------------------
 
+Added
+^^^^^
+- Support ``shtab`` completion of ``Literal`` types (`#693
+  <https://github.com/omni-us/jsonargparse/pull/693>`__).
+
 Changed
 ^^^^^^^
 - ``validate`` now checks values before required so that errors related to wrong
@@ -30,6 +35,9 @@ Fixed
 - Help incorrectly showing environment variable name for ``--print_shtab``.
 - ``add_argument`` raises error when type is assigned with ``action=None``
   (`#687 <https://github.com/omni-us/jsonargparse/issues/687>`__).
+- ``shtab`` failing when parser has positional arguments (`#693
+  <https://github.com/omni-us/jsonargparse/pull/693>`__).
+
 
 v4.37.0 (2025-02-14)
 --------------------
diff --git a/jsonargparse/_completions.py b/jsonargparse/_completions.py
index 21960ed..d6e7cf4 100644
--- a/jsonargparse/_completions.py
+++ b/jsonargparse/_completions.py
@@ -9,7 +9,7 @@ from contextvars import ContextVar
 from enum import Enum
 from importlib.util import find_spec
 from subprocess import PIPE, Popen
-from typing import List, Union
+from typing import List, Literal, Union
 
 from ._actions import ActionConfigFile, _ActionConfigLoad, _ActionHelpClassPath, remove_actions
 from ._parameter_resolvers import get_signature_parameters
@@ -162,7 +162,8 @@ def shtab_prepare_action(action, parser) -> None:
     choices = None
     if isinstance(action, ActionTypeHint):
         skip = getattr(action, "sub_add_kwargs", {}).get("skip", set())
-        choices = get_typehint_choices(action._typehint, action.option_strings[0], parser, skip)
+        prefix = action.option_strings[0] if action.option_strings else None
+        choices = get_typehint_choices(action._typehint, prefix, parser, skip)
         if shtab_shell.get() == "bash":
             message = f"Expected type: {type_to_str(action._typehint)}"
             add_bash_typehint_completion(parser, action, message, choices)
@@ -232,7 +233,9 @@ def get_typehint_choices(typehint, prefix, parser, skip, choices=None, added_sub
         choices.extend(list(typehint.__members__))
     else:
         origin = get_typehint_origin(typehint)
-        if origin == Union:
+        if origin == Literal:
+            choices.extend([str(a) for a in typehint.__args__ if isinstance(a, (str, int, float))])
+        elif origin == Union:
             for subtype in typehint.__args__:
                 if subtype in added_subclasses or subtype is object:
                     continue
@@ -273,18 +276,19 @@ def add_subactions_and_get_subclass_choices(typehint, prefix, parser, skip, adde
                 init_args[param.name].append(param.annotation)
                 subclasses[param.name].append(path.rsplit(".", 1)[-1])
 
-    for name, subtypes in init_args.items():
-        option_string = f"{prefix}.{name}"
-        if option_string not in parser._option_string_actions:
-            action = parser.add_argument(option_string)
-            for subtype in unique(subtypes):
-                subchoices = get_typehint_choices(subtype, option_string, parser, skip, None, added_subclasses)
-                if shtab_shell.get() == "bash":
-                    message = f"Expected type: {type_to_str(subtype)}; "
-                    message += f"Accepted by subclasses: {', '.join(subclasses[name])}"
-                    add_bash_typehint_completion(parser, action, message, subchoices)
-                elif subchoices:
-                    action.choices = subchoices
+    if prefix is not None:
+        for name, subtypes in init_args.items():
+            option_string = f"{prefix}.{name}"
+            if option_string not in parser._option_string_actions:
+                action = parser.add_argument(option_string)
+                for subtype in unique(subtypes):
+                    subchoices = get_typehint_choices(subtype, option_string, parser, skip, None, added_subclasses)
+                    if shtab_shell.get() == "bash":
+                        message = f"Expected type: {type_to_str(subtype)}; "
+                        message += f"Accepted by subclasses: {', '.join(subclasses[name])}"
+                        add_bash_typehint_completion(parser, action, message, subchoices)
+                    elif subchoices:
+                        action.choices = subchoices
 
     return choices
 


Test patch:
diff --git a/jsonargparse_tests/test_shtab.py b/jsonargparse_tests/test_shtab.py
index d26d052..90be331 100644
--- a/jsonargparse_tests/test_shtab.py
+++ b/jsonargparse_tests/test_shtab.py
@@ -5,7 +5,7 @@ from enum import Enum
 from importlib.util import find_spec
 from os import PathLike
 from pathlib import Path
-from typing import Any, Callable, Optional, Union
+from typing import Any, Callable, Literal, Optional, Union
 from unittest.mock import patch
 
 import pytest
@@ -146,6 +146,19 @@ def test_bash_optional_enum(parser, subtests):
     )
 
 
+def test_bash_literal(parser, subtests):
+    typehint = Optional[Literal["one", "two"]]
+    parser.add_argument("--literal", type=typehint)
+    assert_bash_typehint_completions(
+        subtests,
+        parser,
+        [
+            ("literal", typehint, "", ["one", "two", "null"], "3/3"),
+            ("literal", typehint, "t", ["two"], "1/3"),
+        ],
+    )
+
+
 def test_bash_union(parser, subtests):
     typehint = Optional[Union[bool, AXEnum]]
     parser.add_argument("--union", type=typehint)
@@ -159,6 +172,19 @@ def test_bash_union(parser, subtests):
     )
 
 
+def test_bash_positional(parser, subtests):
+    typehint = Literal["Alice", "Bob"]
+    parser.add_argument("name", type=typehint)
+    assert_bash_typehint_completions(
+        subtests,
+        parser,
+        [
+            ("name", typehint, "", ["Alice", "Bob"], "2/2"),
+            ("name", typehint, "Al", ["Alice"], "1/2"),
+        ],
+    )
+
+
 def test_bash_config(parser):
     parser.add_argument("--cfg", action="config")
     shtab_script = get_shtab_script(parser, "bash")