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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
`subcommand_title` or `subcommand_description` still gives the subcommands a dedicated section
([#1715](https://github.com/python-cmd2/cmd2/issues/1715)).
- Fix `@with_annotated` decorator so using `ArgumentBlock` works with groups
- Fixed bug where already sorted `choices_provider` results were being re-sorted.

## 4.1.2 (July 16, 2026)

Expand Down
6 changes: 4 additions & 2 deletions cmd2/argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,7 @@ def print_help(self, tokens: Sequence[str], file: IO[str] | None = None) -> None
self._parser.print_help(file)

def _choices_to_items(self, arg_state: _ArgumentState) -> list[CompletionItem]:
"""Convert choices from action to list of CompletionItems."""
"""Convert an action's choices to a list of CompletionItems."""
if arg_state.action.choices is None:
return []

Expand Down Expand Up @@ -785,13 +785,15 @@ def _complete_arg(
cmd_set,
)
all_choices = list(choices_provider(*args, **kwargs))
sort = False # choices_provider results are already sorted, so don't re-sort them
Comment thread
kmvanbrunt marked this conversation as resolved.
else:
all_choices = self._choices_to_items(arg_state)
sort = True

# Filter used values and run basic completion
used_values = consumed_arg_values.get(arg_state.action.dest, [])
filtered = [choice for choice in all_choices if choice.text not in used_values]
completions = self._cmd_app.basic_complete(text, line, begidx, endidx, filtered)
completions = self._cmd_app.basic_complete(text, line, begidx, endidx, filtered, sort=sort)

return self._build_completion_table(arg_state, completions)

Expand Down
5 changes: 3 additions & 2 deletions examples/modular_commandsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
)

from cmd2 import (
Choices,
Cmd,
Cmd2ArgumentParser,
CommandSet,
Expand All @@ -31,9 +32,9 @@ def __init__(self, command_sets: Iterable[CommandSet] | None = None) -> None:
super().__init__(command_sets=command_sets)
self.sport_item_strs = ["Bat", "Basket", "Basketball", "Football", "Space Ball"]

def choices_provider(self) -> list[str]:
def choices_provider(self) -> Choices:
"""A choices provider is useful when the choice list is based on instance data of your application."""
return self.sport_item_strs
return Choices.from_values(self.sport_item_strs)

# Parser for example command
example_parser = Cmd2ArgumentParser(
Expand Down
52 changes: 52 additions & 0 deletions tests/test_argparse_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1433,3 +1433,55 @@ def test_add_parser_custom_completer() -> None:
name="custom_completer", completer_class=CustomCompleter
)
assert custom_completer_parser.completer_class is CustomCompleter


def test_completer_preserves_custom_order() -> None:
"""Test that completer returning Completions with is_sorted=True preserves custom item ordering."""
custom_order = ("zebra", "apple", "banana")

class CustomApp(cmd2.Cmd):
def custom_completer(self, text: str, line: str, begidx: int, endidx: int) -> Completions:
return Completions.from_values(custom_order, is_sorted=True)

parser = Cmd2ArgumentParser()
parser.add_argument("--custom", completer=custom_completer)

@with_argparser(parser)
def do_test(self, args: argparse.Namespace) -> None:
pass

app = CustomApp()
text = ""
line = f"test --custom {text}"
endidx = len(line)
begidx = endidx - len(text)

completions = app.complete(text, line, begidx, endidx)

assert completions.to_strings() == custom_order


def test_choices_provider_preserves_custom_order() -> None:
"""Test that choices_provider returning Choices with is_sorted=True preserves custom item ordering."""
custom_order = ("zebra", "apple", "banana")

class CustomApp(cmd2.Cmd):
def custom_provider(self) -> Choices:
return Choices.from_values(custom_order, is_sorted=True)

parser = Cmd2ArgumentParser()
parser.add_argument("--custom", choices_provider=custom_provider)

@with_argparser(parser)
def do_test(self, args: argparse.Namespace) -> None:
pass

app = CustomApp()
text = ""
line = f"test --custom {text}"
endidx = len(line)
begidx = endidx - len(text)

completions = app.complete(text, line, begidx, endidx)

assert completions.to_strings() == custom_order
Loading