Skip to content

Commit e7454c6

Browse files
committed
Merge branch 'main' into fix_disable_category
2 parents 818293d + 371205b commit e7454c6

File tree

4 files changed

+86
-37
lines changed

4 files changed

+86
-37
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## 3.1.3 (TBD)
22

33
- Bug Fixes
4+
- Fixed issue where `delimiter_complete()` could cause more matches than display matches
45
- Fixed issue where `CommandSet` registration did not respect disabled categories
56

67
## 3.1.2 (January 26, 2026)

cmd2/cmd2.py

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1766,31 +1766,45 @@ def delimiter_complete(
17661766
:return: a list of possible tab completions
17671767
"""
17681768
matches = self.basic_complete(text, line, begidx, endidx, match_against)
1769+
if not matches:
1770+
return []
17691771

1770-
# Display only the portion of the match that's being completed based on delimiter
1771-
if matches:
1772-
# Set this to True for proper quoting of matches with spaces
1773-
self.matches_delimited = True
1772+
# Set this to True for proper quoting of matches with spaces
1773+
self.matches_delimited = True
17741774

1775-
# Get the common beginning for the matches
1776-
common_prefix = os.path.commonprefix(matches)
1777-
prefix_tokens = common_prefix.split(delimiter)
1775+
# Get the common beginning for the matches
1776+
common_prefix = os.path.commonprefix(matches)
1777+
prefix_tokens = common_prefix.split(delimiter)
17781778

1779-
# Calculate what portion of the match we are completing
1780-
display_token_index = 0
1781-
if prefix_tokens:
1782-
display_token_index = len(prefix_tokens) - 1
1779+
# Calculate what portion of the match we are completing
1780+
display_token_index = 0
1781+
if prefix_tokens:
1782+
display_token_index = len(prefix_tokens) - 1
17831783

1784-
# Get this portion for each match and store them in self.display_matches
1785-
for cur_match in matches:
1786-
match_tokens = cur_match.split(delimiter)
1787-
display_token = match_tokens[display_token_index]
1784+
# Remove from each match everything after where the user is completing.
1785+
# This approach can result in duplicates so we will filter those out.
1786+
unique_results: dict[str, str] = {}
17881787

1789-
if not display_token:
1790-
display_token = delimiter
1791-
self.display_matches.append(display_token)
1788+
for cur_match in matches:
1789+
match_tokens = cur_match.split(delimiter)
17921790

1793-
return matches
1791+
filtered_match = delimiter.join(match_tokens[: display_token_index + 1])
1792+
display_match = match_tokens[display_token_index]
1793+
1794+
# If there are more tokens, then we aren't done completing a full item
1795+
if len(match_tokens) > display_token_index + 1:
1796+
filtered_match += delimiter
1797+
display_match += delimiter
1798+
self.allow_appended_space = False
1799+
self.allow_closing_quote = False
1800+
1801+
if filtered_match not in unique_results:
1802+
unique_results[filtered_match] = display_match
1803+
1804+
filtered_matches = list(unique_results.keys())
1805+
self.display_matches = list(unique_results.values())
1806+
1807+
return filtered_matches
17941808

17951809
def flag_based_complete(
17961810
self,

cmd2/utils.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
"""Shared utility functions."""
22

33
import argparse
4-
import collections
54
import contextlib
65
import functools
76
import glob
@@ -192,10 +191,7 @@ def remove_duplicates(list_to_prune: list[_T]) -> list[_T]:
192191
:param list_to_prune: the list being pruned of duplicates
193192
:return: The pruned list
194193
"""
195-
temp_dict: collections.OrderedDict[_T, Any] = collections.OrderedDict()
196-
for item in list_to_prune:
197-
temp_dict[item] = None
198-
194+
temp_dict = dict.fromkeys(list_to_prune)
199195
return list(temp_dict.keys())
200196

201197

tests/test_completion.py

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -716,19 +716,53 @@ def test_basic_completion_nomatch(cmd2_app) -> None:
716716
assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == []
717717

718718

719-
def test_delimiter_completion(cmd2_app) -> None:
719+
def test_delimiter_completion_partial(cmd2_app) -> None:
720+
"""Test that a delimiter is added when an item has not been fully completed"""
720721
text = '/home/'
721-
line = f'run_script {text}'
722+
line = f'command {text}'
723+
endidx = len(line)
724+
begidx = endidx - len(text)
725+
726+
matches = cmd2_app.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/')
727+
728+
# All matches end with the delimiter
729+
matches.sort(key=cmd2_app.default_sort_key)
730+
expected_matches = sorted(["/home/other user/", "/home/user/"], key=cmd2_app.default_sort_key)
731+
732+
cmd2_app.display_matches.sort(key=cmd2_app.default_sort_key)
733+
expected_display = sorted(["other user/", "user/"], key=cmd2_app.default_sort_key)
734+
735+
assert matches == expected_matches
736+
assert cmd2_app.display_matches == expected_display
737+
738+
739+
def test_delimiter_completion_full(cmd2_app) -> None:
740+
"""Test that no delimiter is added when an item has been fully completed"""
741+
text = '/home/other user/'
742+
line = f'command {text}'
722743
endidx = len(line)
723744
begidx = endidx - len(text)
724745

725-
cmd2_app.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/')
746+
matches = cmd2_app.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/')
747+
748+
# No matches end with the delimiter
749+
matches.sort(key=cmd2_app.default_sort_key)
750+
expected_matches = sorted(["/home/other user/maps", "/home/other user/tests"], key=cmd2_app.default_sort_key)
726751

727-
# Remove duplicates from display_matches and sort it. This is typically done in complete().
728-
display_list = utils.remove_duplicates(cmd2_app.display_matches)
729-
display_list = utils.alphabetical_sort(display_list)
752+
cmd2_app.display_matches.sort(key=cmd2_app.default_sort_key)
753+
expected_display = sorted(["maps", "tests"], key=cmd2_app.default_sort_key)
730754

731-
assert display_list == ['other user', 'user']
755+
assert matches == expected_matches
756+
assert cmd2_app.display_matches == expected_display
757+
758+
759+
def test_delimiter_completion_nomatch(cmd2_app) -> None:
760+
text = '/nothing_to_see'
761+
line = f'command {text}'
762+
endidx = len(line)
763+
begidx = endidx - len(text)
764+
765+
assert cmd2_app.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/') == []
732766

733767

734768
def test_flag_based_completion_single(cmd2_app) -> None:
@@ -964,20 +998,24 @@ def test_add_opening_quote_delimited_no_text(cmd2_app) -> None:
964998
endidx = len(line)
965999
begidx = endidx - len(text)
9661000

967-
# The whole list will be returned with no opening quotes added
1001+
# Matches returned with no opening quote
1002+
expected_matches = sorted(["/home/other user/", "/home/user/"], key=cmd2_app.default_sort_key)
1003+
expected_display = sorted(["other user/", "user/"], key=cmd2_app.default_sort_key)
1004+
9681005
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
9691006
assert first_match is not None
970-
assert cmd2_app.completion_matches == sorted(delimited_strs, key=cmd2_app.default_sort_key)
1007+
assert cmd2_app.completion_matches == expected_matches
1008+
assert cmd2_app.display_matches == expected_display
9711009

9721010

9731011
def test_add_opening_quote_delimited_nothing_added(cmd2_app) -> None:
974-
text = '/ho'
1012+
text = '/home/'
9751013
line = f'test_delimited {text}'
9761014
endidx = len(line)
9771015
begidx = endidx - len(text)
9781016

979-
expected_matches = sorted(delimited_strs, key=cmd2_app.default_sort_key)
980-
expected_display = sorted(['other user', 'user'], key=cmd2_app.default_sort_key)
1017+
expected_matches = sorted(['/home/other user/', '/home/user/'], key=cmd2_app.default_sort_key)
1018+
expected_display = sorted(['other user/', 'user/'], key=cmd2_app.default_sort_key)
9811019

9821020
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
9831021
assert first_match is not None
@@ -1017,7 +1055,7 @@ def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app) -> None:
10171055

10181056

10191057
def test_add_opening_quote_delimited_space_in_prefix(cmd2_app) -> None:
1020-
# This test when a space appears before the part of the string that is the display match
1058+
# This tests when a space appears before the part of the string that is the display match
10211059
text = '/home/oth'
10221060
line = f'test_delimited {text}'
10231061
endidx = len(line)

0 commit comments

Comments
 (0)