Skip to content

Commit c1f8d8f

Browse files
authored
shtab support literal and fix positionals (#693)
1 parent 745eac8 commit c1f8d8f

File tree

3 files changed

+54
-16
lines changed

3 files changed

+54
-16
lines changed

CHANGELOG.rst

+8
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ paths are considered internals and can change in minor and patch releases.
1515
v4.38.0 (2025-02-??)
1616
--------------------
1717

18+
Added
19+
^^^^^
20+
- Support ``shtab`` completion of ``Literal`` types (`#693
21+
<https://github.com/omni-us/jsonargparse/pull/693>`__).
22+
1823
Changed
1924
^^^^^^^
2025
- ``validate`` now checks values before required so that errors related to wrong
@@ -30,6 +35,9 @@ Fixed
3035
- Help incorrectly showing environment variable name for ``--print_shtab``.
3136
- ``add_argument`` raises error when type is assigned with ``action=None``
3237
(`#687 <https://github.com/omni-us/jsonargparse/issues/687>`__).
38+
- ``shtab`` failing when parser has positional arguments (`#693
39+
<https://github.com/omni-us/jsonargparse/pull/693>`__).
40+
3341

3442
v4.37.0 (2025-02-14)
3543
--------------------

jsonargparse/_completions.py

+19-15
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from enum import Enum
1010
from importlib.util import find_spec
1111
from subprocess import PIPE, Popen
12-
from typing import List, Union
12+
from typing import List, Literal, Union
1313

1414
from ._actions import ActionConfigFile, _ActionConfigLoad, _ActionHelpClassPath, remove_actions
1515
from ._parameter_resolvers import get_signature_parameters
@@ -162,7 +162,8 @@ def shtab_prepare_action(action, parser) -> None:
162162
choices = None
163163
if isinstance(action, ActionTypeHint):
164164
skip = getattr(action, "sub_add_kwargs", {}).get("skip", set())
165-
choices = get_typehint_choices(action._typehint, action.option_strings[0], parser, skip)
165+
prefix = action.option_strings[0] if action.option_strings else None
166+
choices = get_typehint_choices(action._typehint, prefix, parser, skip)
166167
if shtab_shell.get() == "bash":
167168
message = f"Expected type: {type_to_str(action._typehint)}"
168169
add_bash_typehint_completion(parser, action, message, choices)
@@ -232,7 +233,9 @@ def get_typehint_choices(typehint, prefix, parser, skip, choices=None, added_sub
232233
choices.extend(list(typehint.__members__))
233234
else:
234235
origin = get_typehint_origin(typehint)
235-
if origin == Union:
236+
if origin == Literal:
237+
choices.extend([str(a) for a in typehint.__args__ if isinstance(a, (str, int, float))])
238+
elif origin == Union:
236239
for subtype in typehint.__args__:
237240
if subtype in added_subclasses or subtype is object:
238241
continue
@@ -273,18 +276,19 @@ def add_subactions_and_get_subclass_choices(typehint, prefix, parser, skip, adde
273276
init_args[param.name].append(param.annotation)
274277
subclasses[param.name].append(path.rsplit(".", 1)[-1])
275278

276-
for name, subtypes in init_args.items():
277-
option_string = f"{prefix}.{name}"
278-
if option_string not in parser._option_string_actions:
279-
action = parser.add_argument(option_string)
280-
for subtype in unique(subtypes):
281-
subchoices = get_typehint_choices(subtype, option_string, parser, skip, None, added_subclasses)
282-
if shtab_shell.get() == "bash":
283-
message = f"Expected type: {type_to_str(subtype)}; "
284-
message += f"Accepted by subclasses: {', '.join(subclasses[name])}"
285-
add_bash_typehint_completion(parser, action, message, subchoices)
286-
elif subchoices:
287-
action.choices = subchoices
279+
if prefix is not None:
280+
for name, subtypes in init_args.items():
281+
option_string = f"{prefix}.{name}"
282+
if option_string not in parser._option_string_actions:
283+
action = parser.add_argument(option_string)
284+
for subtype in unique(subtypes):
285+
subchoices = get_typehint_choices(subtype, option_string, parser, skip, None, added_subclasses)
286+
if shtab_shell.get() == "bash":
287+
message = f"Expected type: {type_to_str(subtype)}; "
288+
message += f"Accepted by subclasses: {', '.join(subclasses[name])}"
289+
add_bash_typehint_completion(parser, action, message, subchoices)
290+
elif subchoices:
291+
action.choices = subchoices
288292

289293
return choices
290294

jsonargparse_tests/test_shtab.py

+27-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from importlib.util import find_spec
66
from os import PathLike
77
from pathlib import Path
8-
from typing import Any, Callable, Optional, Union
8+
from typing import Any, Callable, Literal, Optional, Union
99
from unittest.mock import patch
1010

1111
import pytest
@@ -146,6 +146,19 @@ def test_bash_optional_enum(parser, subtests):
146146
)
147147

148148

149+
def test_bash_literal(parser, subtests):
150+
typehint = Optional[Literal["one", "two"]]
151+
parser.add_argument("--literal", type=typehint)
152+
assert_bash_typehint_completions(
153+
subtests,
154+
parser,
155+
[
156+
("literal", typehint, "", ["one", "two", "null"], "3/3"),
157+
("literal", typehint, "t", ["two"], "1/3"),
158+
],
159+
)
160+
161+
149162
def test_bash_union(parser, subtests):
150163
typehint = Optional[Union[bool, AXEnum]]
151164
parser.add_argument("--union", type=typehint)
@@ -159,6 +172,19 @@ def test_bash_union(parser, subtests):
159172
)
160173

161174

175+
def test_bash_positional(parser, subtests):
176+
typehint = Literal["Alice", "Bob"]
177+
parser.add_argument("name", type=typehint)
178+
assert_bash_typehint_completions(
179+
subtests,
180+
parser,
181+
[
182+
("name", typehint, "", ["Alice", "Bob"], "2/2"),
183+
("name", typehint, "Al", ["Alice"], "1/2"),
184+
],
185+
)
186+
187+
162188
def test_bash_config(parser):
163189
parser.add_argument("--cfg", action="config")
164190
shtab_script = get_shtab_script(parser, "bash")

0 commit comments

Comments
 (0)