@@ -56,6 +56,13 @@ def do_paint(
5656- ``tuple[T, T]`` (fixed arity, same type) -- ``nargs=N`` with ``type=T``
5757- ``T | None`` -- unwrapped to ``T``, treated as optional
5858
59+ Action compatibility note:
60+
61+ - Some argparse actions (``count``, ``store_true``, ``store_false``,
62+ ``store_const``, ``help``, ``version``) do not accept ``type=``.
63+ If one of these actions is selected via ``Option(action=...)``, any
64+ inferred ``type`` converter is removed before calling ``add_argument()``.
65+
5966Unsupported patterns (raise ``TypeError``):
6067
6168- ``str | int`` -- union of multiple non-None types is ambiguous
@@ -66,9 +73,8 @@ def do_paint(
6673*inside*: ``Annotated[T | None, meta]``. Writing
6774``Annotated[T, meta] | None`` is ambiguous and raises ``TypeError``.
6875
69- Note: ``Path`` and ``Enum`` types also get automatic tab completion via
70- ``ArgparseCompleter`` type inference. This works for both ``@with_annotated``
71- and ``@with_argparser`` -- see the ``argparse_completer`` module.
76+ Note: ``Path`` and ``Enum`` annotations with ``@with_annotated`` also get
77+ automatic tab completion via generated parser metadata.
7278If a user-supplied ``choices_provider`` or ``completer`` is set on an argument,
7379it always takes priority over the type-inferred completion.
7480"""
@@ -78,9 +84,9 @@ def do_paint(
7884import enum
7985import functools
8086import inspect
81- import pathlib
8287import types
8388from collections .abc import Callable , Container
89+ from pathlib import Path
8490from typing import (
8591 Annotated ,
8692 Any ,
@@ -92,6 +98,8 @@ def do_paint(
9298 get_type_hints ,
9399)
94100
101+ from .cmd2 import Cmd
102+ from .completion import CompletionItem
95103from .types import ChoicesProviderUnbound , CmdOrSet , CompleterUnbound
96104
97105# ---------------------------------------------------------------------------
@@ -110,6 +118,7 @@ class _BaseArgMetadata:
110118 'completer' : 'completer' ,
111119 'table_columns' : 'table_columns' ,
112120 'suppress_tab_hint' : 'suppress_tab_hint' ,
121+ 'nargs' : 'nargs' ,
113122 }
114123
115124 def __init__ (
@@ -177,6 +186,15 @@ def __init__(
177186 self .action = action
178187 self .required = required
179188
189+ def to_kwargs (self ) -> dict [str , Any ]:
190+ """Return non-None fields as an argparse kwargs dict."""
191+ kwargs = super ().to_kwargs ()
192+ if self .action :
193+ kwargs ['action' ] = self .action
194+ if self .required :
195+ kwargs ['required' ] = self .required
196+ return kwargs
197+
180198
181199#: Metadata extracted from ``Annotated[T, meta]``, or ``None`` for plain types.
182200ArgMetadata = Argument | Option | None
@@ -196,8 +214,12 @@ def __init__(
196214# before passing to argparse.
197215# ---------------------------------------------------------------------------
198216
199- _BOOL_TRUE_VALUES = {'1' , 'true' , 't' , 'yes' , 'y' , 'on' }
200- _BOOL_FALSE_VALUES = {'0' , 'false' , 'f' , 'no' , 'n' , 'off' }
217+ _BOOL_TRUE_VALUES = ['1' , 'true' , 't' , 'yes' , 'y' , 'on' ]
218+ _BOOL_FALSE_VALUES = ['0' , 'false' , 'f' , 'no' , 'n' , 'off' ]
219+ _ACTIONS_DISALLOW_TYPE = frozenset ({'count' , 'store_true' , 'store_false' , 'store_const' , 'help' , 'version' })
220+ _BOOL_CHOICES = [CompletionItem (True , text = text ) for text in _BOOL_TRUE_VALUES ] + [
221+ CompletionItem (False , text = text ) for text in _BOOL_FALSE_VALUES
222+ ]
201223
202224
203225def _parse_bool (value : str ) -> bool :
@@ -296,6 +318,11 @@ def _resolve(_tp: Any, _args: tuple[Any, ...], **_ctx: Any) -> dict[str, Any]:
296318 return _resolve
297319
298320
321+ def _resolve_path (_tp : Any , _args : tuple [Any , ...], ** _ctx : Any ) -> dict [str , Any ]:
322+ """Resolve Path and add completer."""
323+ return {'type' : Path , 'completer' : Cmd .path_complete }
324+
325+
299326def _resolve_bool (
300327 _tp : Any ,
301328 _args : tuple [Any , ...],
@@ -310,7 +337,7 @@ def _resolve_bool(
310337 if action_str :
311338 return {'action' : action_str , 'is_bool_flag' : True }
312339 return {'action' : argparse .BooleanOptionalAction , 'is_bool_flag' : True }
313- return {'type' : _parse_bool }
340+ return {'type' : _parse_bool , 'choices' : list ( _BOOL_CHOICES ) }
314341
315342
316343def _resolve_element (tp : Any ) -> tuple [Any , dict [str , Any ]]:
@@ -392,7 +419,10 @@ def _resolve_literal(_tp: Any, args: tuple[Any, ...], **_ctx: Any) -> dict[str,
392419
393420def _resolve_enum (tp : Any , _args : tuple [Any , ...], ** _ctx : Any ) -> dict [str , Any ]:
394421 """Resolve Enum subclasses into converter + choices."""
395- return {'type' : _make_enum_type (tp ), 'choices' : [m .value for m in tp ]}
422+ return {
423+ 'type' : _make_enum_type (tp ),
424+ 'choices' : [CompletionItem (m , text = str (m .value ), display_meta = m .name ) for m in tp ],
425+ }
396426
397427
398428# -- Registry -----------------------------------------------------------------
@@ -401,7 +431,7 @@ def _resolve_enum(tp: Any, _args: tuple[Any, ...], **_ctx: Any) -> dict[str, Any
401431 # Subclass-matchable entries first -- iteration order matters for the
402432 # issubclass fallback. enum.Enum must precede int (IntEnum <: int).
403433 enum .Enum : _resolve_enum ,
404- pathlib . Path : _make_simple_resolver ( pathlib . Path ) ,
434+ Path : _resolve_path ,
405435 # Exact-match entries (order among these doesn't affect subclass lookup).
406436 bool : _resolve_bool ,
407437 int : _make_simple_resolver (int ),
@@ -456,13 +486,16 @@ def _resolve_type(
456486
457487 if metadata :
458488 kwargs .update (metadata .to_kwargs ())
459- if metadata .nargs is not None :
460- kwargs ['nargs' ] = metadata .nargs
461489
462- if (has_default and default is not None ) or has_default :
490+ # Some argparse actions (e.g. count/store_true) do not accept a type converter.
491+ action_name = kwargs .get ('action' )
492+ if isinstance (action_name , str ) and action_name in _ACTIONS_DISALLOW_TYPE :
493+ kwargs .pop ('type' , None )
494+
495+ if has_default :
463496 kwargs ['default' ] = default
464497
465- if ( is_kw_only and not has_default ) or ( isinstance ( metadata , Option ) and metadata . required ) :
498+ if is_kw_only and not has_default :
466499 kwargs ['required' ] = True
467500
468501 if kwargs .get ('choices_provider' ) or kwargs .get ('completer' ):
0 commit comments