Skip to content

Commit 2118018

Browse files
chore: more feedback
1 parent 1ad0ca1 commit 2118018

File tree

4 files changed

+105
-172
lines changed

4 files changed

+105
-172
lines changed

cmd2/annotated.py

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
5966
Unsupported 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.
7278
If a user-supplied ``choices_provider`` or ``completer`` is set on an argument,
7379
it always takes priority over the type-inferred completion.
7480
"""
@@ -78,9 +84,9 @@ def do_paint(
7884
import enum
7985
import functools
8086
import inspect
81-
import pathlib
8287
import types
8388
from collections.abc import Callable, Container
89+
from pathlib import Path
8490
from 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
95103
from .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.
182200
ArgMetadata = 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

203225
def _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+
299326
def _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

316343
def _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

393420
def _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'):

cmd2/argparse_completer.py

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@
55

66
import argparse
77
import dataclasses
8-
import enum
98
import inspect
10-
import pathlib
119
from collections import (
1210
defaultdict,
1311
deque,
@@ -717,16 +715,6 @@ def print_help(self, tokens: Sequence[str], file: IO[str] | None = None) -> None
717715

718716
def _choices_to_items(self, arg_state: _ArgumentState) -> list[CompletionItem]:
719717
"""Convert choices from action to list of CompletionItems."""
720-
action_type = arg_state.action.type
721-
if action_type is not None and arg_state.action.choices is None:
722-
if isinstance(action_type, type) and issubclass(action_type, enum.Enum):
723-
return [CompletionItem(str(m.value), display_meta=m.name) for m in action_type]
724-
enum_from_converter = getattr(action_type, '_cmd2_enum_class', None)
725-
if isinstance(enum_from_converter, type) and issubclass(enum_from_converter, enum.Enum):
726-
return [CompletionItem(str(m.value), display_meta=m.name) for m in enum_from_converter]
727-
if getattr(action_type, '__name__', None) == '_parse_bool':
728-
return [CompletionItem(v) for v in ['true', 'false', 'yes', 'no', 'on', 'off', '1', '0']]
729-
730718
if arg_state.action.choices is None:
731719
return []
732720

@@ -804,13 +792,6 @@ def _complete_arg(
804792
)
805793
args.extend([text, line, begidx, endidx])
806794
completions: Completions = completer(*args, **kwargs)
807-
# if is a path type, then use cmd2's path completer
808-
elif arg_state.action.type is pathlib.Path or (
809-
isinstance(arg_state.action.type, type) and issubclass(arg_state.action.type, pathlib.Path)
810-
):
811-
from .cmd2 import Cmd
812-
813-
completions = Cmd.path_complete(self._cmd2_app, text, line, begidx, endidx)
814795

815796
# Otherwise it uses a choices provider or choices list
816797
else:

docs/features/argument_processing.md

Lines changed: 12 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,11 @@ Both `Argument` and `Option` accept the same cmd2-specific fields as `add_argume
153153
`Option` additionally accepts `action`, `required`, and positional `*names` for custom flag strings
154154
(e.g. `Option("--color", "-c")`).
155155

156+
When an `Option(action=...)` uses an argparse action that does not accept `type=` (`count`,
157+
`store_true`, `store_false`, `store_const`, `help`, `version`), `@with_annotated` removes any
158+
inferred `type` converter before calling `add_argument()`. This matches argparse behavior and avoids
159+
parser-construction errors such as combining `action='count'` with `type=int`.
160+
156161
### Comparison with @with_argparser
157162

158163
The two decorators are interchangeable. Here is the same command written both ways:
@@ -254,29 +259,16 @@ def do_raw(self, text: str):
254259

255260
## Automatic Completion from Types
256261

257-
When an argparse argument has `type=Path` or `type=MyEnum` set -- whether manually via
258-
`add_argument()` or automatically via `@with_annotated` -- the completer will provide tab completion
259-
without needing an explicit `choices_provider` or `completer`.
260-
261-
This applies to both `@with_argparser` and `@with_annotated`:
262-
263-
- `type=pathlib.Path` (or any `Path` subclass) triggers filesystem path completion
264-
- `type=MyEnum` (any `enum.Enum` subclass) triggers completion from enum member values
265-
266-
For example, with `@with_argparser`:
262+
With `@with_annotated`, arguments annotated as `Path` or `Enum` get automatic completion without
263+
needing an explicit `choices_provider` or `completer`.
267264

268-
```py
269-
parser = Cmd2ArgumentParser()
270-
parser.add_argument('filepath', type=Path)
271-
parser.add_argument('color', type=MyColorEnum)
265+
Specifically:
272266

273-
@with_argparser(parser)
274-
def do_load(self, args):
275-
... # filepath gets path completion, color gets enum completion
276-
```
267+
- `Path` (or any `Path` subclass) triggers filesystem path completion
268+
- `MyEnum` (any `enum.Enum` subclass) triggers completion from enum member values
277269

278-
With `@with_annotated`, the same inference happens because `Path` and `Enum` annotations generate
279-
the equivalent parser configuration automatically.
270+
With `@with_argparser`, provide `choices`, `choices_provider`, or `completer` explicitly when you
271+
want completion behavior.
280272

281273
## Argument Parsing
282274

0 commit comments

Comments
 (0)