Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions cmd2/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,16 @@ class CompletionItem:
# control sequences (like ^J or ^I) in the completion menu.
_CONTROL_WHITESPACE_RE = re.compile(r"\r\n|[\n\r\t\f\v]")

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

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

# Optional string for displaying the completion differently in the completion menu.
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ each:
- Example that demonstrates the `CommandSet` features for modularizing commands and demonstrates
all main capabilities including basic CommandSets, dynamic loading an unloading, using
subcommands, etc.
- [completion_item_choices.py](https://github.com/python-cmd2/cmd2/blob/main/examples/completion_item_choices.py)
- Demonstrates using CompletionItem instances as elements in an argparse choices list.
- [custom_parser.py](https://github.com/python-cmd2/cmd2/blob/main/examples/custom_parser.py)
- Demonstrates how to create your own custom `Cmd2ArgumentParser`
- [custom_types.py](https://github.com/python-cmd2/cmd2/blob/main/examples/custom_types.py)
Expand Down
145 changes: 145 additions & 0 deletions examples/completion_item_choices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#!/usr/bin/env python
"""
Demonstrates using CompletionItem instances as elements in an argparse choices list.

Technical Note:
Using 'choices' is best for fixed datasets that do not change during the
application's lifecycle. For dynamic data (e.g., results from a database or
file system), use a 'choices_provider' instead.

Key strengths of this approach:
1. Command handlers receive fully-typed domain objects directly in the
argparse.Namespace, eliminating manual lookups from string keys.
2. Choices carry tab-completion UI enhancements (display_meta, table_data)
that are not supported by standard argparse string choices.
3. Provides a single source of truth for completion UI, input validation,
and object mapping.

This demo showcases two distinct approaches:
1. Simple: Using CompletionItems with basic types (ints) to add UI metadata
(display_meta) while letting argparse handle standard type conversion.
2. Advanced: Using a custom 'text' alias and a type converter to map a friendly
string (e.g., 'alice') directly to a complex object (Account).
"""

import argparse
import sys
from typing import (
ClassVar,
cast,
)

from cmd2 import (
Cmd,
Cmd2ArgumentParser,
CompletionItem,
with_argparser,
)

# -----------------------------------------------------------------------------
# Simple Example: Basic types with UI metadata
# -----------------------------------------------------------------------------
# Integers with metadata. No 'text' override or custom type converter needed.
# argparse will handle 'type=int' and validate it against the CompletionItem.value.
id_choices = [
CompletionItem(101, display_meta="Alice's Account"),
CompletionItem(202, display_meta="Bob's Account"),
]


# -----------------------------------------------------------------------------
# Advanced Example: Mapping friendly aliases to objects
# -----------------------------------------------------------------------------
class Account:
"""A complex object that we want to select by a friendly name."""

def __init__(self, account_id: int, owner: str):
self.account_id = account_id
self.owner = owner

def __eq__(self, other: object) -> bool:
if isinstance(other, Account):
return self.account_id == other.account_id
return False

def __hash__(self) -> int:
return hash(self.account_id)

def __repr__(self) -> str:
return f"Account(id={self.account_id}, owner='{self.owner}')"


# Map friendly 'text' aliases to the actual object 'value'.
# The user types 'alice' or 'bob' (tab-completion), but the parsed value will be the Account object.
accounts = [
Account(101, "Alice"),
Account(202, "Bob"),
]
account_choices = [
CompletionItem(
acc,
text=acc.owner.lower(),
display_meta=f"ID: {acc.account_id}",
)
for acc in accounts
]


def account_lookup(name: str) -> Account:
"""Type converter that looks up an Account by its friendly name."""
for item in account_choices:
if item.text == name:
return cast(Account, item.value)
raise argparse.ArgumentTypeError(f"invalid account: {name}")


# -----------------------------------------------------------------------------
# Demo Application
# -----------------------------------------------------------------------------
class ChoicesDemo(Cmd):
"""Demo cmd2 application."""

DEFAULT_CATEGORY: ClassVar[str] = "Demo Commands"

def __init__(self) -> None:
super().__init__()
self.intro = (
"Welcome to the CompletionItem Choices Demo!\n"
"Try 'simple' followed by [TAB] to see basic metadata.\n"
"Try 'advanced' followed by [TAB] to see custom string mapping."
)

# Simple Command: argparse handles the int conversion, CompletionItem handles the UI
simple_parser = Cmd2ArgumentParser()
simple_parser.add_argument(
"account_id",
type=int,
choices=id_choices,
help="Select an account ID (tab-complete to see metadata)",
)

@with_argparser(simple_parser)
def do_simple(self, args: argparse.Namespace) -> None:
Comment thread
tleonhardt marked this conversation as resolved.
"""Show an account ID selection (Simple Case)."""
# argparse converted the input to an int, and validated it against the CompletionItem.value
self.poutput(f"Selected Account ID: {args.account_id} (Type: {type(args.account_id).__name__})")

# Advanced Command: Custom lookup and custom 'text' mapping
advanced_parser = Cmd2ArgumentParser()
advanced_parser.add_argument(
"account",
type=account_lookup,
choices=account_choices,
help="Select an account by owner name (tab-complete to see friendly names)",
)

@with_argparser(advanced_parser)
def do_advanced(self, args: argparse.Namespace) -> None:
"""Show a custom string selection (Advanced Case)."""
# args.account is the full Account object
self.poutput(f"Selected Account: {args.account!r} (Type: {type(args.account).__name__})")


if __name__ == "__main__":
app = ChoicesDemo()
sys.exit(app.cmdloop())
Loading