Skip to content

Commit 47c492b

Browse files
authored
Added example for using CompletionItems as elements in an argparse choices list. (#1644)
1 parent 74a2889 commit 47c492b

3 files changed

Lines changed: 155 additions & 6 deletions

File tree

cmd2/completion.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,16 @@ class CompletionItem:
4949
# control sequences (like ^J or ^I) in the completion menu.
5050
_CONTROL_WHITESPACE_RE = re.compile(r"\r\n|[\n\r\t\f\v]")
5151

52-
# The core object this completion represents (e.g., str, int, Path).
53-
# This serves as the default source for the completion string and is used
54-
# to support object-based validation when used in argparse choices.
52+
# The source input for the completion. This is used to initialize the 'text'
53+
# field (defaults to str(value)). The original object is also preserved to
54+
# support object-based validation when this CompletionItem is used as an
55+
# argparse choice.
5556
value: Any = field(kw_only=False)
5657

57-
# The actual completion string. If not provided, defaults to str(value).
58-
# This can be used to provide a human-friendly alias for complex objects in
59-
# an argparse choices list (requires a matching 'type' converter for validation).
58+
# The string matched against user input and inserted into the command line.
59+
# Defaults to str(value). This should only be set manually if this
60+
# CompletionItem is used as an argparse choice and you want the choice
61+
# string to differ from str(value).
6062
text: str = _UNSET_STR
6163

6264
# Optional string for displaying the completion differently in the completion menu.

examples/README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ each:
3434
- Example that demonstrates the `CommandSet` features for modularizing commands and demonstrates
3535
all main capabilities including basic CommandSets, dynamic loading an unloading, using
3636
subcommands, etc.
37+
- [completion_item_choices.py](https://github.com/python-cmd2/cmd2/blob/main/examples/completion_item_choices.py)
38+
- Demonstrates using CompletionItem instances as elements in an argparse choices list.
3739
- [custom_parser.py](https://github.com/python-cmd2/cmd2/blob/main/examples/custom_parser.py)
3840
- Demonstrates how to create your own custom `Cmd2ArgumentParser`
3941
- [custom_types.py](https://github.com/python-cmd2/cmd2/blob/main/examples/custom_types.py)
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env python
2+
"""
3+
Demonstrates using CompletionItem instances as elements in an argparse choices list.
4+
5+
Technical Note:
6+
Using 'choices' is best for fixed datasets that do not change during the
7+
application's lifecycle. For dynamic data (e.g., results from a database or
8+
file system), use a 'choices_provider' instead.
9+
10+
Key strengths of this approach:
11+
1. Command handlers receive fully-typed domain objects directly in the
12+
argparse.Namespace, eliminating manual lookups from string keys.
13+
2. Choices carry tab-completion UI enhancements (display_meta, table_data)
14+
that are not supported by standard argparse string choices.
15+
3. Provides a single source of truth for completion UI, input validation,
16+
and object mapping.
17+
18+
This demo showcases two distinct approaches:
19+
1. Simple: Using CompletionItems with basic types (ints) to add UI metadata
20+
(display_meta) while letting argparse handle standard type conversion.
21+
2. Advanced: Using a custom 'text' alias and a type converter to map a friendly
22+
string (e.g., 'alice') directly to a complex object (Account).
23+
"""
24+
25+
import argparse
26+
import sys
27+
from typing import (
28+
ClassVar,
29+
cast,
30+
)
31+
32+
from cmd2 import (
33+
Cmd,
34+
Cmd2ArgumentParser,
35+
CompletionItem,
36+
with_argparser,
37+
)
38+
39+
# -----------------------------------------------------------------------------
40+
# Simple Example: Basic types with UI metadata
41+
# -----------------------------------------------------------------------------
42+
# Integers with metadata. No 'text' override or custom type converter needed.
43+
# argparse will handle 'type=int' and validate it against the CompletionItem.value.
44+
id_choices = [
45+
CompletionItem(101, display_meta="Alice's Account"),
46+
CompletionItem(202, display_meta="Bob's Account"),
47+
]
48+
49+
50+
# -----------------------------------------------------------------------------
51+
# Advanced Example: Mapping friendly aliases to objects
52+
# -----------------------------------------------------------------------------
53+
class Account:
54+
"""A complex object that we want to select by a friendly name."""
55+
56+
def __init__(self, account_id: int, owner: str):
57+
self.account_id = account_id
58+
self.owner = owner
59+
60+
def __eq__(self, other: object) -> bool:
61+
if isinstance(other, Account):
62+
return self.account_id == other.account_id
63+
return False
64+
65+
def __hash__(self) -> int:
66+
return hash(self.account_id)
67+
68+
def __repr__(self) -> str:
69+
return f"Account(id={self.account_id}, owner='{self.owner}')"
70+
71+
72+
# Map friendly 'text' aliases to the actual object 'value'.
73+
# The user types 'alice' or 'bob' (tab-completion), but the parsed value will be the Account object.
74+
accounts = [
75+
Account(101, "Alice"),
76+
Account(202, "Bob"),
77+
]
78+
account_choices = [
79+
CompletionItem(
80+
acc,
81+
text=acc.owner.lower(),
82+
display_meta=f"ID: {acc.account_id}",
83+
)
84+
for acc in accounts
85+
]
86+
87+
88+
def account_lookup(name: str) -> Account:
89+
"""Type converter that looks up an Account by its friendly name."""
90+
for item in account_choices:
91+
if item.text == name:
92+
return cast(Account, item.value)
93+
raise argparse.ArgumentTypeError(f"invalid account: {name}")
94+
95+
96+
# -----------------------------------------------------------------------------
97+
# Demo Application
98+
# -----------------------------------------------------------------------------
99+
class ChoicesDemo(Cmd):
100+
"""Demo cmd2 application."""
101+
102+
DEFAULT_CATEGORY: ClassVar[str] = "Demo Commands"
103+
104+
def __init__(self) -> None:
105+
super().__init__()
106+
self.intro = (
107+
"Welcome to the CompletionItem Choices Demo!\n"
108+
"Try 'simple' followed by [TAB] to see basic metadata.\n"
109+
"Try 'advanced' followed by [TAB] to see custom string mapping."
110+
)
111+
112+
# Simple Command: argparse handles the int conversion, CompletionItem handles the UI
113+
simple_parser = Cmd2ArgumentParser()
114+
simple_parser.add_argument(
115+
"account_id",
116+
type=int,
117+
choices=id_choices,
118+
help="Select an account ID (tab-complete to see metadata)",
119+
)
120+
121+
@with_argparser(simple_parser)
122+
def do_simple(self, args: argparse.Namespace) -> None:
123+
"""Show an account ID selection (Simple Case)."""
124+
# argparse converted the input to an int, and validated it against the CompletionItem.value
125+
self.poutput(f"Selected Account ID: {args.account_id} (Type: {type(args.account_id).__name__})")
126+
127+
# Advanced Command: Custom lookup and custom 'text' mapping
128+
advanced_parser = Cmd2ArgumentParser()
129+
advanced_parser.add_argument(
130+
"account",
131+
type=account_lookup,
132+
choices=account_choices,
133+
help="Select an account by owner name (tab-complete to see friendly names)",
134+
)
135+
136+
@with_argparser(advanced_parser)
137+
def do_advanced(self, args: argparse.Namespace) -> None:
138+
"""Show a custom string selection (Advanced Case)."""
139+
# args.account is the full Account object
140+
self.poutput(f"Selected Account: {args.account!r} (Type: {type(args.account).__name__})")
141+
142+
143+
if __name__ == "__main__":
144+
app = ChoicesDemo()
145+
sys.exit(app.cmdloop())

0 commit comments

Comments
 (0)