From 85176fa6462d3e805581605b0f6a8c0fc40a1471 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 22 Apr 2026 02:43:15 -0400 Subject: [PATCH 1/3] Added example for using CompletionItem instances as elements in an argparse choices list. --- cmd2/completion.py | 12 +-- examples/README.md | 2 + examples/completion_item_choices.py | 132 ++++++++++++++++++++++++++++ 3 files changed, 140 insertions(+), 6 deletions(-) create mode 100755 examples/completion_item_choices.py diff --git a/cmd2/completion.py b/cmd2/completion.py index fff0e999d..3c55c04bc 100644 --- a/cmd2/completion.py +++ b/cmd2/completion.py @@ -49,14 +49,14 @@ 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 underlying object this completion represents (e.g., str, int, Path). + # This serves as the default source for 'text' and is used to support + # object-based validation when this item 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 actual completion string. Defaults to str(value). This should only be + # set manually if this item 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. diff --git a/examples/README.md b/examples/README.md index 32f2549ed..46ba97f23 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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) diff --git a/examples/completion_item_choices.py b/examples/completion_item_choices.py new file mode 100755 index 000000000..06e490f21 --- /dev/null +++ b/examples/completion_item_choices.py @@ -0,0 +1,132 @@ +#!/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 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 __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.""" + + 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: + """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()) From 255c94240b464b6e6eb3aca413b1ec381a171d8c Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 22 Apr 2026 03:28:51 -0400 Subject: [PATCH 2/3] Updated example. --- examples/completion_item_choices.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/examples/completion_item_choices.py b/examples/completion_item_choices.py index 06e490f21..3213ae00c 100755 --- a/examples/completion_item_choices.py +++ b/examples/completion_item_choices.py @@ -54,6 +54,14 @@ 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}')" From 788272ea00a11f51e8a2773a52e85d058d133151 Mon Sep 17 00:00:00 2001 From: Kevin Van Brunt Date: Wed, 22 Apr 2026 12:02:59 -0400 Subject: [PATCH 3/3] Updated comments and example code. --- cmd2/completion.py | 14 ++++++++------ examples/completion_item_choices.py | 7 ++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/cmd2/completion.py b/cmd2/completion.py index 3c55c04bc..e161c88f0 100644 --- a/cmd2/completion.py +++ b/cmd2/completion.py @@ -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 underlying object this completion represents (e.g., str, int, Path). - # This serves as the default source for 'text' and is used to support - # object-based validation when this item is used as an argparse choice. + # 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. Defaults to str(value). This should only be - # set manually if this item is used as an argparse choice and you want the - # choice string to differ from str(value). + # 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. diff --git a/examples/completion_item_choices.py b/examples/completion_item_choices.py index 3213ae00c..a69116b9c 100755 --- a/examples/completion_item_choices.py +++ b/examples/completion_item_choices.py @@ -24,7 +24,10 @@ import argparse import sys -from typing import cast +from typing import ( + ClassVar, + cast, +) from cmd2 import ( Cmd, @@ -96,6 +99,8 @@ def account_lookup(name: str) -> Account: class ChoicesDemo(Cmd): """Demo cmd2 application.""" + DEFAULT_CATEGORY: ClassVar[str] = "Demo Commands" + def __init__(self) -> None: super().__init__() self.intro = (