@@ -254,7 +254,7 @@ class TestBuildParser:
254254 pytest .param (_func_kw_only , {"option_strings" : ["--name" ], "required" : True }, id = "kw_only_required" ),
255255 pytest .param (_func_kw_only_with_default , {"option_strings" : ["--name" ], "default" : "world" }, id = "kw_only_default" ),
256256 # --- Underscore in flag names ---
257- pytest .param (_func_underscore_option , {"option_strings" : ["--my_param " ], "default" : "x" }, id = "underscore_flag" ),
257+ pytest .param (_func_underscore_option , {"option_strings" : ["--my-param " ], "default" : "x" }, id = "underscore_flag" ),
258258 # --- Default type preservation ---
259259 pytest .param (
260260 _func_default_type_mismatch , {"option_strings" : ["--count" ], "default" : "1" }, id = "default_not_coerced"
@@ -365,6 +365,28 @@ def test_choices_provider_overrides_inferred_enum_choices(self) -> None:
365365 assert action .get_choices_provider () is not None # type: ignore[attr-defined]
366366 assert action .get_completer () is None # type: ignore[attr-defined]
367367
368+ def test_choices_provider_strips_strict_enum_converter (self ) -> None :
369+ """User-supplied choices_provider on Enum drops the restrictive enum converter."""
370+ action = _get_param_action (_func_choices_provider_on_enum )
371+ assert action .type is None
372+
373+ def test_choices_provider_strips_strict_literal_converter (self ) -> None :
374+ """User-supplied choices_provider on Literal drops the restrictive literal converter."""
375+
376+ def func (
377+ self ,
378+ mode : Annotated [Literal ["fast" , "slow" ], Argument (choices_provider = _provider )],
379+ ) -> None : ...
380+
381+ action = _get_param_action (func )
382+ assert action .type is None
383+ assert action .choices is None
384+
385+ def test_completer_keeps_path_converter (self ) -> None :
386+ """User-supplied completer on Path preserves the (non-restrictive) Path converter."""
387+ action = _get_param_action (_func_completer_on_path )
388+ assert action .type is Path
389+
368390 def test_completer_overrides_inferred_path_completion (self ) -> None :
369391 action = _get_param_action (_func_completer_on_path )
370392 assert action .get_choices_provider () is None # type: ignore[attr-defined]
@@ -558,7 +580,7 @@ def test_apply_mutex_group_targets_rejects_cross_group_members(self) -> None:
558580
559581
560582# ---------------------------------------------------------------------------
561- # _resolve_annotation: positional vs option classification + bool flag
583+ # _resolve_annotation: positional vs option classification
562584# ---------------------------------------------------------------------------
563585
564586_ARG_META = Argument (help_text = "Name" )
@@ -567,39 +589,38 @@ def test_apply_mutex_group_targets_rejects_cross_group_members(self) -> None:
567589
568590class TestResolveAnnotation :
569591 @pytest .mark .parametrize (
570- ("annotation" , "has_default" , "expected_positional" , "expected_bool_flag" ),
592+ ("annotation" , "has_default" , "expected_positional" ),
571593 [
572- pytest .param (str , False , True , False , id = "plain_str" ),
573- pytest .param (str | None , False , False , False , id = "optional_str" ),
574- pytest .param (Annotated [str , _ARG_META ], False , True , False , id = "annotated_argument" ),
575- pytest .param (Annotated [str , _OPT_META ], False , False , False , id = "annotated_option" ),
576- pytest .param (Annotated [str , "some doc" ], False , True , False , id = "annotated_no_meta" ),
577- pytest .param (str , True , False , False , id = "has_default" ),
578- pytest .param (bool , True , False , True , id = "bool_flag" ),
594+ pytest .param (str , False , True , id = "plain_str" ),
595+ pytest .param (str | None , False , False , id = "optional_str" ),
596+ pytest .param (Annotated [str , _ARG_META ], False , True , id = "annotated_argument" ),
597+ pytest .param (Annotated [str , _OPT_META ], False , False , id = "annotated_option" ),
598+ pytest .param (Annotated [str , "some doc" ], False , True , id = "annotated_no_meta" ),
599+ pytest .param (str , True , False , id = "has_default" ),
600+ pytest .param (bool , True , False , id = "bool_flag" ),
579601 ],
580602 )
581- def test_classification (self , annotation , has_default , expected_positional , expected_bool_flag ) -> None :
582- _kwargs , _meta , positional , is_bool_flag = _resolve_annotation (annotation , has_default = has_default )
603+ def test_classification (self , annotation , has_default , expected_positional ) -> None :
604+ _kwargs , _meta , positional = _resolve_annotation (annotation , has_default = has_default )
583605 assert positional is expected_positional
584- assert is_bool_flag is expected_bool_flag
585606
586607 def test_optional_wrapping_annotated_with_none_inside (self ) -> None :
587608 """Optional[Annotated[T | None, meta]] is allowed (inner type contains None)."""
588609 ann = Annotated [str | None , _OPT_META ] | None
589- _kwargs , meta , positional , _bf = _resolve_annotation (ann )
610+ _kwargs , meta , positional = _resolve_annotation (ann )
590611 assert meta is _OPT_META
591612 assert positional is False
592613
593614 def test_typing_union_optional (self ) -> None :
594615 ns : dict = {}
595616 exec ("import typing; t = typing.Union[str, None]" , ns )
596- _kwargs , _meta , positional , _bool_flag = _resolve_annotation (ns ["t" ])
617+ _kwargs , _meta , positional = _resolve_annotation (ns ["t" ])
597618 assert positional is False
598619
599620 def test_annotated_multiple_metadata_picks_first (self ) -> None :
600621 meta1 = Argument (help_text = "first" )
601622 meta2 = Option ("--x" , help_text = "second" )
602- kwargs , meta , _ , _ = _resolve_annotation (Annotated [str , meta1 , meta2 ])
623+ kwargs , meta , _ = _resolve_annotation (Annotated [str , meta1 , meta2 ])
603624 assert meta is meta1
604625 assert kwargs .get ("help" ) == "first"
605626
@@ -639,7 +660,7 @@ def test_nested_collection_raises(self, annotation) -> None:
639660 ],
640661 )
641662 def test_unsupported_collection_no_nargs (self , annotation ) -> None :
642- kwargs , _ , _ , _ = _resolve_annotation (annotation )
663+ kwargs , _ , _ = _resolve_annotation (annotation )
643664 assert "nargs" not in kwargs
644665 assert "action" not in kwargs
645666
@@ -1085,7 +1106,9 @@ def do_transfer(
10851106
10861107@pytest .fixture
10871108def app () -> _IntegrationApp :
1088- return _IntegrationApp ()
1109+ app = _IntegrationApp ()
1110+ app .stdout = cmd2 .utils .StdSim (app .stdout )
1111+ return app
10891112
10901113
10911114@pytest .fixture
@@ -1147,12 +1170,29 @@ def test_ns_provider(self, app) -> None:
11471170 assert app .ns_calls == 1
11481171
11491172 def test_cmd2_prefixed_param_is_preserved (self , app ) -> None :
1150- out , _err = run_cmd (app , "prefixed --cmd2_mode 5" )
1173+ out , _err = run_cmd (app , "prefixed --cmd2-mode 5" )
11511174 assert out == ["cmd2_mode=5" ]
11521175
11531176 def test_kwargs_passthrough (self , app ) -> None :
11541177 app .do_greet ("Alice" , keyword_arg = "kwarg_value" )
11551178
1179+ def test_direct_call_with_positional_only (self , app ) -> None :
1180+ """Calling do_* directly with a single statement string parses normally."""
1181+ app .do_greet ("Alice" )
1182+ assert app .stdout .getvalue ().splitlines ()[- 1 ] == "Hello Alice"
1183+
1184+ def test_direct_call_with_options (self , app ) -> None :
1185+ """Direct call with a full statement string including options."""
1186+ app .do_greet ("Alice --count 2 --loud" )
1187+ out = app .stdout .getvalue ().splitlines ()
1188+ assert out [- 2 :] == ["HELLO ALICE" , "HELLO ALICE" ]
1189+
1190+ def test_direct_call_kwargs_override_parsed (self , app ) -> None :
1191+ """Explicit kwargs on a direct call override parsed values."""
1192+ app .do_greet ("Alice" , count = 3 )
1193+ out = app .stdout .getvalue ().splitlines ()
1194+ assert out [- 3 :] == ["Hello Alice" , "Hello Alice" , "Hello Alice" ]
1195+
11561196 def test_bare_call_decorator (self ) -> None :
11571197 """@with_annotated() with empty parens works same as @with_annotated."""
11581198
@@ -1176,7 +1216,7 @@ def test_missing_parser_raises(self, app) -> None:
11761216
11771217class TestGroupedParserIntegration :
11781218 def test_grouped_command_executes (self , grouped_app ) -> None :
1179- out , _err = run_cmd (grouped_app , "transfer --local build.tar.gz --dry_run " )
1219+ out , _err = run_cmd (grouped_app , "transfer --local build.tar.gz --dry-run " )
11801220 assert out == ["Transfer build.tar.gz in dry-run mode" ]
11811221
11821222 def test_grouped_command_mutex_error (self , grouped_app ) -> None :
@@ -1189,7 +1229,7 @@ def test_grouped_command_help_lists_flags(self, grouped_app) -> None:
11891229 assert "--local" in help_text
11901230 assert "--remote" in help_text
11911231 assert "--force" in help_text
1192- assert "--dry_run " in help_text
1232+ assert "--dry-run " in help_text
11931233
11941234
11951235# ---------------------------------------------------------------------------
@@ -1293,6 +1333,14 @@ def test_base_command_missing_handler_raises(self) -> None:
12931333 def do_bad (self , verbose : bool = False ) -> None :
12941334 pass
12951335
1336+ def test_cmd2_handler_without_base_command_raises (self ) -> None :
1337+ """A 'cmd2_handler' parameter is only valid when base_command=True."""
1338+ with pytest .raises (TypeError , match = "base_command=True" ):
1339+
1340+ @cmd2 .with_annotated
1341+ def do_bad (self , cmd2_handler , name : str = "" ) -> None :
1342+ pass
1343+
12961344 @pytest .mark .parametrize (
12971345 "kwargs" ,
12981346 [
0 commit comments