Skip to content

Commit 4fc726c

Browse files
chore: a few extra test
1 parent e6790c3 commit 4fc726c

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

tests/test_annotated.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from typing import (
1313
Annotated,
1414
Literal,
15+
Optional,
1516
)
1617

1718
import pytest
@@ -79,6 +80,10 @@ class _PlainColor(enum.Enum):
7980
]
8081

8182

83+
class _Port(int):
84+
"""Subclass of ``int`` used to verify subclass fallback in type resolution."""
85+
86+
8287
# ---------------------------------------------------------------------------
8388
# Single-parameter test functions for build_parser_from_function.
8489
# Each has exactly one param (besides self) so dest is auto-derived.
@@ -134,6 +139,19 @@ def _func_plain_enum(self, color: _PlainColor) -> None: ...
134139
def _func_list_int(self, nums: list[int]) -> None: ...
135140
def _func_set_int(self, nums: set[int]) -> None: ...
136141
def _func_tuple_fixed_triple(self, triple: tuple[int, int, int]) -> None: ...
142+
def _func_list_bool(self, flags: list[bool]) -> None: ...
143+
def _func_set_bool(self, flags: set[bool]) -> None: ...
144+
def _func_list_path(self, files: list[Path]) -> None: ...
145+
def _func_list_enum(self, colors: list[_Color]) -> None: ...
146+
def _func_list_literal(self, modes: list[Literal["fast", "slow"]]) -> None: ...
147+
def _func_tuple_paths(self, src_dst: tuple[Path, Path]) -> None: ...
148+
def _func_tuple_enums(self, pair: tuple[_Color, _Color]) -> None: ...
149+
def _func_optional_str_nondefault(self, name: str | None = "world") -> None: ...
150+
def _func_typing_optional(self, count: Optional[int] = None) -> None: ... # noqa: UP045
151+
def _func_int_subclass(self, port: _Port) -> None: ...
152+
def _func_store_true_action(self, verbose: Annotated[bool, Option("--verbose", action="store_true")] = False) -> None: ...
153+
def _func_store_false_action(self, quiet: Annotated[bool, Option("--quiet", action="store_false")] = True) -> None: ...
154+
def _func_append_action(self, tag: Annotated[str | None, Option("--tag", action="append")] = None) -> None: ...
137155
def _func_multi(self, a: str, b: int, c: int = 1) -> None: ...
138156
def _func_grouped(
139157
self,
@@ -287,6 +305,40 @@ class TestBuildParser:
287305
{"option_strings": ["--name"], "default": None},
288306
id="optional_annotated_inside",
289307
),
308+
# --- Collections of complex element types ---
309+
pytest.param(_func_list_bool, {"option_strings": [], "nargs": "+", "type": _parse_bool}, id="list_bool"),
310+
pytest.param(_func_set_bool, {"option_strings": [], "nargs": "+", "type": _parse_bool}, id="set_bool"),
311+
pytest.param(_func_list_path, {"option_strings": [], "nargs": "+", "type": Path}, id="list_path"),
312+
pytest.param(
313+
_func_list_literal,
314+
{"option_strings": [], "nargs": "+", "choices": ["fast", "slow"]},
315+
id="list_literal",
316+
),
317+
pytest.param(
318+
_func_list_enum,
319+
{"option_strings": [], "nargs": "+", "choices": _COLOR_CHOICE_ITEMS},
320+
id="list_enum",
321+
),
322+
pytest.param(_func_tuple_paths, {"option_strings": [], "nargs": 2, "type": Path}, id="tuple_paths"),
323+
pytest.param(
324+
_func_tuple_enums,
325+
{"option_strings": [], "nargs": 2, "choices": _COLOR_CHOICE_ITEMS},
326+
id="tuple_enums",
327+
),
328+
# --- Subclass fallback (Port(int) uses int converter) ---
329+
pytest.param(_func_int_subclass, {"option_strings": [], "type": int}, id="int_subclass"),
330+
# --- Optional with non-None default ---
331+
pytest.param(
332+
_func_optional_str_nondefault,
333+
{"option_strings": ["--name"], "default": "world"},
334+
id="optional_str_nondefault",
335+
),
336+
# --- typing.Optional[T] (vs T | None) end-to-end ---
337+
pytest.param(
338+
_func_typing_optional,
339+
{"option_strings": ["--count"], "type": int, "default": None},
340+
id="typing_optional",
341+
),
290342
],
291343
)
292344
def test_action_attributes(self, func, expected) -> None:
@@ -303,6 +355,26 @@ def test_annotated_action_count_non_bool(self) -> None:
303355
assert isinstance(action, argparse._CountAction)
304356
assert action.default == 0
305357

358+
def test_annotated_action_store_true(self) -> None:
359+
"""``action='store_true'`` strips the inferred bool converter."""
360+
action = _get_param_action(_func_store_true_action)
361+
assert isinstance(action, argparse._StoreTrueAction)
362+
assert action.type is None
363+
assert action.default is False
364+
365+
def test_annotated_action_store_false(self) -> None:
366+
"""``action='store_false'`` strips the inferred bool converter."""
367+
action = _get_param_action(_func_store_false_action)
368+
assert isinstance(action, argparse._StoreFalseAction)
369+
assert action.type is None
370+
assert action.default is True
371+
372+
def test_annotated_action_append(self) -> None:
373+
"""``action='append'`` collects repeated flag values into a list."""
374+
action = _get_param_action(_func_append_action)
375+
assert isinstance(action, argparse._AppendAction)
376+
assert action.option_strings == ["--tag"]
377+
306378
@pytest.mark.parametrize(
307379
"func",
308380
[
@@ -420,6 +492,20 @@ def test_inferred_enum_choices_match_type_converter(self) -> None:
420492
for choice in action.choices:
421493
assert isinstance(converter(str(choice)), _Color)
422494

495+
@pytest.mark.parametrize(
496+
"func",
497+
[
498+
pytest.param(_func_path, id="path_positional"),
499+
pytest.param(_func_path_option, id="path_option"),
500+
pytest.param(_func_list_path, id="list_path"),
501+
pytest.param(_func_tuple_paths, id="tuple_paths"),
502+
],
503+
)
504+
def test_path_annotation_wires_path_completer(self, func) -> None:
505+
"""A bare ``Path`` annotation (no user metadata) auto-wires ``Cmd.path_complete``."""
506+
action = _get_param_action(func)
507+
assert action.get_completer() is cmd2.Cmd.path_complete # type: ignore[attr-defined]
508+
423509

424510
# ---------------------------------------------------------------------------
425511
# Argument groups and mutually exclusive groups
@@ -988,6 +1074,50 @@ def test_non_list_passthrough(self) -> None:
9881074
assert ns.items == "single_value"
9891075

9901076

1077+
class TestCollectionRuntimeCast:
1078+
"""End-to-end verify ``parse_args`` returns the declared container type, not a plain list."""
1079+
1080+
def test_set_int_returns_set(self) -> None:
1081+
parser = build_parser_from_function(_func_set_int)
1082+
ns = parser.parse_args(["1", "2", "2", "3"])
1083+
assert isinstance(ns.nums, set)
1084+
assert ns.nums == {1, 2, 3}
1085+
1086+
def test_tuple_ellipsis_returns_tuple(self) -> None:
1087+
parser = build_parser_from_function(_func_tuple_ellipsis)
1088+
ns = parser.parse_args(["1", "2", "3"])
1089+
assert isinstance(ns.values, tuple)
1090+
assert ns.values == (1, 2, 3)
1091+
1092+
def test_tuple_fixed_returns_tuple(self) -> None:
1093+
parser = build_parser_from_function(_func_tuple_fixed)
1094+
ns = parser.parse_args(["5", "10"])
1095+
assert isinstance(ns.pair, tuple)
1096+
assert ns.pair == (5, 10)
1097+
1098+
def test_list_bool_returns_list_of_bools(self) -> None:
1099+
parser = build_parser_from_function(_func_list_bool)
1100+
ns = parser.parse_args(["true", "no", "on"])
1101+
assert ns.flags == [True, False, True]
1102+
1103+
def test_tuple_paths_returns_tuple_of_paths(self) -> None:
1104+
parser = build_parser_from_function(_func_tuple_paths)
1105+
ns = parser.parse_args(["/tmp/a", "/tmp/b"])
1106+
assert isinstance(ns.src_dst, tuple)
1107+
assert ns.src_dst == (Path("/tmp/a"), Path("/tmp/b"))
1108+
1109+
def test_append_action_collects_values(self) -> None:
1110+
parser = build_parser_from_function(_func_append_action)
1111+
ns = parser.parse_args(["--tag", "a", "--tag", "b"])
1112+
assert ns.tag == ["a", "b"]
1113+
1114+
def test_int_subclass_uses_int_converter(self) -> None:
1115+
"""``Port(int)`` falls back to ``int`` converter; argparse returns ``int``, not ``Port``."""
1116+
parser = build_parser_from_function(_func_int_subclass)
1117+
ns = parser.parse_args(["8080"])
1118+
assert ns.port == 8080
1119+
1120+
9911121
# ---------------------------------------------------------------------------
9921122
# _filtered_namespace_kwargs edge cases
9931123
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)