|
| 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