Skip to content

Commit 85176fa

Browse files
committed
Added example for using CompletionItem instances as elements in an argparse choices list.
1 parent 74a2889 commit 85176fa

3 files changed

Lines changed: 140 additions & 6 deletions

File tree

cmd2/completion.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ 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 underlying object this completion represents (e.g., str, int, Path).
53+
# This serves as the default source for 'text' and is used to support
54+
# object-based validation when this item is used as an argparse choice.
5555
value: Any = field(kw_only=False)
5656

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).
57+
# The actual completion string. Defaults to str(value). This should only be
58+
# set manually if this item is used as an argparse choice and you want the
59+
# choice string to differ from str(value).
6060
text: str = _UNSET_STR
6161

6262
# 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: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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 cast
28+
29+
from cmd2 import (
30+
Cmd,
31+
Cmd2ArgumentParser,
32+
CompletionItem,
33+
with_argparser,
34+
)
35+
36+
# -----------------------------------------------------------------------------
37+
# Simple Example: Basic types with UI metadata
38+
# -----------------------------------------------------------------------------
39+
# Integers with metadata. No 'text' override or custom type converter needed.
40+
# argparse will handle 'type=int' and validate it against the CompletionItem.value.
41+
id_choices = [
42+
CompletionItem(101, display_meta="Alice's Account"),
43+
CompletionItem(202, display_meta="Bob's Account"),
44+
]
45+
46+
47+
# -----------------------------------------------------------------------------
48+
# Advanced Example: Mapping friendly aliases to objects
49+
# -----------------------------------------------------------------------------
50+
class Account:
51+
"""A complex object that we want to select by a friendly name."""
52+
53+
def __init__(self, account_id: int, owner: str):
54+
self.account_id = account_id
55+
self.owner = owner
56+
57+
def __repr__(self) -> str:
58+
return f"Account(id={self.account_id}, owner='{self.owner}')"
59+
60+
61+
# Map friendly 'text' aliases to the actual object 'value'.
62+
# The user types 'alice' or 'bob' (tab-completion), but the parsed value will be the Account object.
63+
accounts = [
64+
Account(101, "Alice"),
65+
Account(202, "Bob"),
66+
]
67+
account_choices = [
68+
CompletionItem(
69+
acc,
70+
text=acc.owner.lower(),
71+
display_meta=f"ID: {acc.account_id}",
72+
)
73+
for acc in accounts
74+
]
75+
76+
77+
def account_lookup(name: str) -> Account:
78+
"""Type converter that looks up an Account by its friendly name."""
79+
for item in account_choices:
80+
if item.text == name:
81+
return cast(Account, item.value)
82+
raise argparse.ArgumentTypeError(f"invalid account: {name}")
83+
84+
85+
# -----------------------------------------------------------------------------
86+
# Demo Application
87+
# -----------------------------------------------------------------------------
88+
class ChoicesDemo(Cmd):
89+
"""Demo cmd2 application."""
90+
91+
def __init__(self) -> None:
92+
super().__init__()
93+
self.intro = (
94+
"Welcome to the CompletionItem Choices Demo!\n"
95+
"Try 'simple' followed by [TAB] to see basic metadata.\n"
96+
"Try 'advanced' followed by [TAB] to see custom string mapping."
97+
)
98+
99+
# Simple Command: argparse handles the int conversion, CompletionItem handles the UI
100+
simple_parser = Cmd2ArgumentParser()
101+
simple_parser.add_argument(
102+
"account_id",
103+
type=int,
104+
choices=id_choices,
105+
help="Select an account ID (tab-complete to see metadata)",
106+
)
107+
108+
@with_argparser(simple_parser)
109+
def do_simple(self, args: argparse.Namespace) -> None:
110+
"""Show an account ID selection (Simple Case)."""
111+
# argparse converted the input to an int, and validated it against the CompletionItem.value
112+
self.poutput(f"Selected Account ID: {args.account_id} (Type: {type(args.account_id).__name__})")
113+
114+
# Advanced Command: Custom lookup and custom 'text' mapping
115+
advanced_parser = Cmd2ArgumentParser()
116+
advanced_parser.add_argument(
117+
"account",
118+
type=account_lookup,
119+
choices=account_choices,
120+
help="Select an account by owner name (tab-complete to see friendly names)",
121+
)
122+
123+
@with_argparser(advanced_parser)
124+
def do_advanced(self, args: argparse.Namespace) -> None:
125+
"""Show a custom string selection (Advanced Case)."""
126+
# args.account is the full Account object
127+
self.poutput(f"Selected Account: {args.account!r} (Type: {type(args.account).__name__})")
128+
129+
130+
if __name__ == "__main__":
131+
app = ChoicesDemo()
132+
sys.exit(app.cmdloop())

0 commit comments

Comments
 (0)