Skip to content

Commit fb575e4

Browse files
committed
Merge remote-tracking branch 'origin/master' into argparse_remainder
2 parents 6d79731 + 467be57 commit fb575e4

File tree

12 files changed

+134
-91
lines changed

12 files changed

+134
-91
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
have been deleted
4242
* The new application lifecycle hook system allows for registration of callbacks to be called at various points
4343
in the lifecycle and is more powerful and flexible than the previous system
44-
* ``alias`` is now a command with subcommands to create, list, and delete aliases. Therefore its syntax
44+
* ``alias`` is now a command with sub-commands to create, list, and delete aliases. Therefore its syntax
4545
has changed. All current alias commands in startup scripts or transcripts will break with this release.
4646
* `unalias` was deleted since ``alias delete`` replaced it
4747

cmd2/argparse_completer.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,13 @@ def set_custom_message(self, custom_message: str='') -> None:
965965
self._custom_error_message = custom_message
966966
# End cmd2 customization
967967

968+
def add_subparsers(self, **kwargs):
969+
"""Custom override. Sets a default title if one was not given."""
970+
if 'title' not in kwargs:
971+
kwargs['title'] = 'sub-commands'
972+
973+
return super().add_subparsers(**kwargs)
974+
968975
def error(self, message: str) -> None:
969976
"""Custom error override. Allows application to control the error being displayed by argparse"""
970977
if len(self._custom_error_message) > 0:

cmd2/cmd2.py

Lines changed: 44 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -548,8 +548,8 @@ def __init__(self, completekey: str='tab', stdin=None, stdout=None, persistent_h
548548
self.exit_code = None
549549

550550
# This lock should be acquired before doing any asynchronous changes to the terminal to
551-
# ensure the updates to the terminal don't interfere with the input being typed. It can be
552-
# acquired any time there is a readline prompt on screen.
551+
# ensure the updates to the terminal don't interfere with the input being typed or output
552+
# being printed by a command.
553553
self.terminal_lock = threading.RLock()
554554

555555
# ----- Methods related to presenting output to the user -----
@@ -2204,7 +2204,7 @@ def _cmdloop(self) -> bool:
22042204

22052205
return stop
22062206

2207-
# ----- Alias subcommand functions -----
2207+
# ----- Alias sub-command functions -----
22082208

22092209
def alias_create(self, args: argparse.Namespace):
22102210
"""Create or overwrite an alias"""
@@ -2267,8 +2267,8 @@ def alias_list(self, args: argparse.Namespace):
22672267
" macro")
22682268
alias_parser = ACArgumentParser(description=alias_description, epilog=alias_epilog, prog='alias')
22692269

2270-
# Add subcommands to alias
2271-
alias_subparsers = alias_parser.add_subparsers(title='sub-commands')
2270+
# Add sub-commands to alias
2271+
alias_subparsers = alias_parser.add_subparsers()
22722272

22732273
# alias -> create
22742274
alias_create_help = "create or overwrite an alias"
@@ -2326,13 +2326,13 @@ def do_alias(self, args: argparse.Namespace):
23262326
"""Manage aliases"""
23272327
func = getattr(args, 'func', None)
23282328
if func is not None:
2329-
# Call whatever subcommand function was selected
2329+
# Call whatever sub-command function was selected
23302330
func(self, args)
23312331
else:
2332-
# No subcommand was provided, so call help
2332+
# No sub-command was provided, so call help
23332333
self.do_help('alias')
23342334

2335-
# ----- Macro subcommand functions -----
2335+
# ----- Macro sub-command functions -----
23362336

23372337
def macro_create(self, args: argparse.Namespace):
23382338
"""Create or overwrite a macro"""
@@ -2445,8 +2445,8 @@ def macro_list(self, args: argparse.Namespace):
24452445
" alias")
24462446
macro_parser = ACArgumentParser(description=macro_description, epilog=macro_epilog, prog='macro')
24472447

2448-
# Add subcommands to macro
2449-
macro_subparsers = macro_parser.add_subparsers(title='sub-commands')
2448+
# Add sub-commands to macro
2449+
macro_subparsers = macro_parser.add_subparsers()
24502450

24512451
# macro -> create
24522452
macro_create_help = "create or overwrite a macro"
@@ -2527,10 +2527,10 @@ def do_macro(self, args: argparse.Namespace):
25272527
"""Manage macros"""
25282528
func = getattr(args, 'func', None)
25292529
if func is not None:
2530-
# Call whatever subcommand function was selected
2530+
# Call whatever sub-command function was selected
25312531
func(self, args)
25322532
else:
2533-
# No subcommand was provided, so call help
2533+
# No sub-command was provided, so call help
25342534
self.do_help('macro')
25352535

25362536
def complete_help_command(self, text: str, line: str, begidx: int, endidx: int) -> List[str]:
@@ -2551,7 +2551,7 @@ def complete_help_subcommand(self, text: str, line: str, begidx: int, endidx: in
25512551
if not tokens:
25522552
return []
25532553

2554-
# Must have at least 3 args for 'help command subcommand'
2554+
# Must have at least 3 args for 'help command sub-command'
25552555
if len(tokens) < 3:
25562556
return []
25572557

@@ -2580,7 +2580,7 @@ def complete_help_subcommand(self, text: str, line: str, begidx: int, endidx: in
25802580

25812581
setattr(help_parser.add_argument('command', help="command to retrieve help for", nargs="?"),
25822582
ACTION_ARG_CHOICES, ('complete_help_command',))
2583-
setattr(help_parser.add_argument('subcommand', help="subcommand to retrieve help for",
2583+
setattr(help_parser.add_argument('subcommand', help="sub-command to retrieve help for",
25842584
nargs=argparse.REMAINDER),
25852585
ACTION_ARG_CHOICES, ('complete_help_subcommand',))
25862586
help_parser.add_argument('-v', '--verbose', action='store_true',
@@ -3481,12 +3481,13 @@ def _clear_input_lines_str(self) -> str: # pragma: no cover
34813481

34823482
def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None: # pragma: no cover
34833483
"""
3484-
Used to display an important message to the user while they are at the prompt in between commands.
3484+
Display an important message to the user while they are at the prompt in between commands.
34853485
To the user it appears as if an alert message is printed above the prompt and their current input
34863486
text and cursor location is left alone.
34873487
3488-
IMPORTANT: Do not call this unless you have acquired self.terminal_lock
3489-
first, which ensures a prompt is onscreen
3488+
IMPORTANT: This function will not print an alert unless it can acquire self.terminal_lock to ensure
3489+
a prompt is onscreen. Therefore it is best to acquire the lock before calling this function
3490+
to guarantee the alert prints.
34903491
34913492
:param alert_msg: the message to display to the user
34923493
:param new_prompt: if you also want to change the prompt that is displayed, then include it here
@@ -3525,34 +3526,48 @@ def async_alert(self, alert_msg: str, new_prompt: Optional[str] = None) -> None:
35253526

35263527
def async_update_prompt(self, new_prompt: str) -> None: # pragma: no cover
35273528
"""
3528-
Updates the prompt while the user is still typing at it. This is good for alerting the user to system
3529+
Update the prompt while the user is still typing at it. This is good for alerting the user to system
35293530
changes dynamically in between commands. For instance you could alter the color of the prompt to indicate
35303531
a system status or increase a counter to report an event. If you do alter the actual text of the prompt,
35313532
it is best to keep the prompt the same width as what's on screen. Otherwise the user's input text will
35323533
be shifted and the update will not be seamless.
35333534
3534-
IMPORTANT: Do not call this unless you have acquired self.terminal_lock
3535-
first, which ensures a prompt is onscreen
3535+
IMPORTANT: This function will not update the prompt unless it can acquire self.terminal_lock to ensure
3536+
a prompt is onscreen. Therefore it is best to acquire the lock before calling this function
3537+
to guarantee the prompt changes.
35363538
35373539
:param new_prompt: what to change the prompt to
3540+
:raises RuntimeError if called while another thread holds terminal_lock
35383541
"""
35393542
self.async_alert('', new_prompt)
35403543

3541-
@staticmethod
3542-
def set_window_title(title: str) -> None: # pragma: no cover
3544+
def set_window_title(self, title: str) -> None: # pragma: no cover
35433545
"""
3544-
Sets the terminal window title
3546+
Set the terminal window title
3547+
3548+
IMPORTANT: This function will not set the title unless it can acquire self.terminal_lock to avoid
3549+
writing to stderr while a command is running. Therefore it is best to acquire the lock
3550+
before calling this function to guarantee the title changes.
3551+
35453552
:param title: the new window title
3553+
:raises RuntimeError if called while another thread holds terminal_lock
35463554
"""
35473555
if not vt100_support:
35483556
return
35493557

3550-
import colorama.ansi as ansi
3551-
try:
3552-
sys.stderr.write(ansi.set_title(title))
3553-
except AttributeError:
3554-
# Debugging in Pycharm has issues with setting terminal title
3555-
pass
3558+
# Sanity check that can't fail if self.terminal_lock was acquired before calling this function
3559+
if self.terminal_lock.acquire(blocking=False):
3560+
try:
3561+
import colorama.ansi as ansi
3562+
sys.stderr.write(ansi.set_title(title))
3563+
except AttributeError:
3564+
# Debugging in Pycharm has issues with setting terminal title
3565+
pass
3566+
3567+
self.terminal_lock.release()
3568+
3569+
else:
3570+
raise RuntimeError("another thread holds terminal_lock")
35563571

35573572
def cmdloop(self, intro: Optional[str]=None) -> None:
35583573
"""This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2.

cmd2/pyscript_bridge.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,16 @@ class CommandResult(namedtuple_with_defaults('CommandResult', ['stdout', 'stderr
3333
3434
NOTE: Named tuples are immutable. So the contents are there for access, not for modification.
3535
"""
36-
def __bool__(self):
37-
"""If stderr is None and data is not None the command is considered a success"""
38-
return not self.stderr and self.data is not None
36+
def __bool__(self) -> bool:
37+
"""Returns True if the command succeeded, otherwise False"""
38+
39+
# If data has a __bool__ method, then call it to determine success of command
40+
if self.data is not None and callable(getattr(self.data, '__bool__', None)):
41+
return bool(self.data)
42+
43+
# Otherwise check if stderr was filled out
44+
else:
45+
return not self.stderr
3946

4047

4148
def _exec_cmd(cmd2_app, func: Callable, echo: bool) -> CommandResult:
@@ -91,7 +98,7 @@ def __dir__(self):
9198
return commands
9299

93100
def __getattr__(self, item: str):
94-
"""Search for a subcommand matching this item and update internal state to track the traversal"""
101+
"""Search for a sub-command matching this item and update internal state to track the traversal"""
95102
# look for sub-command under the current command/sub-command layer
96103
for action in self.__current_subcommand_parser._actions:
97104
if not action.option_strings and isinstance(action, argparse._SubParsersAction):

examples/subcommands.py

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#!/usr/bin/env python3
22
# coding=utf-8
3-
"""A simple example demonstrating how to use Argparse to support subcommands.
3+
"""A simple example demonstrating how to use Argparse to support sub-commands.
44
55
6-
This example shows an easy way for a single command to have many subcommands, each of which takes different arguments
6+
This example shows an easy way for a single command to have many sub-commands, each of which takes different arguments
77
and provides separate contextual help.
88
"""
99
import argparse
@@ -13,15 +13,15 @@
1313

1414
# create the top-level parser for the base command
1515
base_parser = argparse.ArgumentParser(prog='base')
16-
base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help')
16+
base_subparsers = base_parser.add_subparsers(title='sub-commands', help='sub-command help')
1717

18-
# create the parser for the "foo" subcommand
18+
# create the parser for the "foo" sub-command
1919
parser_foo = base_subparsers.add_parser('foo', help='foo help')
2020
parser_foo.add_argument('-x', type=int, default=1, help='integer')
2121
parser_foo.add_argument('y', type=float, help='float')
2222
parser_foo.add_argument('input_file', type=str, help='Input File')
2323

24-
# create the parser for the "bar" subcommand
24+
# create the parser for the "bar" sub-command
2525
parser_bar = base_subparsers.add_parser('bar', help='bar help')
2626

2727
bar_subparsers = parser_bar.add_subparsers(title='layer3', help='help for 3rd layer of commands')
@@ -31,7 +31,7 @@
3131
bar_subparsers.add_parser('artichoke', help='artichoke help')
3232
bar_subparsers.add_parser('cranberries', help='cranberries help')
3333

34-
# create the parser for the "sport" subcommand
34+
# create the parser for the "sport" sub-command
3535
parser_sport = base_subparsers.add_parser('sport', help='sport help')
3636
sport_arg = parser_sport.add_argument('sport', help='Enter name of a sport')
3737
setattr(sport_arg, 'arg_choices', sport_item_strs)
@@ -40,15 +40,15 @@
4040
# create the top-level parser for the alternate command
4141
# The alternate command doesn't provide its own help flag
4242
base2_parser = argparse.ArgumentParser(prog='alternate', add_help=False)
43-
base2_subparsers = base2_parser.add_subparsers(title='subcommands', help='subcommand help')
43+
base2_subparsers = base2_parser.add_subparsers(title='sub-commands', help='sub-command help')
4444

45-
# create the parser for the "foo" subcommand
45+
# create the parser for the "foo" sub-command
4646
parser_foo2 = base2_subparsers.add_parser('foo', help='foo help')
4747
parser_foo2.add_argument('-x', type=int, default=1, help='integer')
4848
parser_foo2.add_argument('y', type=float, help='float')
4949
parser_foo2.add_argument('input_file', type=str, help='Input File')
5050

51-
# create the parser for the "bar" subcommand
51+
# create the parser for the "bar" sub-command
5252
parser_bar2 = base2_subparsers.add_parser('bar', help='bar help')
5353

5454
bar2_subparsers = parser_bar2.add_subparsers(title='layer3', help='help for 3rd layer of commands')
@@ -58,34 +58,34 @@
5858
bar2_subparsers.add_parser('artichoke', help='artichoke help')
5959
bar2_subparsers.add_parser('cranberries', help='cranberries help')
6060

61-
# create the parser for the "sport" subcommand
61+
# create the parser for the "sport" sub-command
6262
parser_sport2 = base2_subparsers.add_parser('sport', help='sport help')
6363
sport2_arg = parser_sport2.add_argument('sport', help='Enter name of a sport')
6464
setattr(sport2_arg, 'arg_choices', sport_item_strs)
6565

6666

6767
class SubcommandsExample(cmd2.Cmd):
6868
"""
69-
Example cmd2 application where we a base command which has a couple subcommands
70-
and the "sport" subcommand has tab completion enabled.
69+
Example cmd2 application where we a base command which has a couple sub-commands
70+
and the "sport" sub-command has tab completion enabled.
7171
"""
7272
def __init__(self):
7373
super().__init__()
7474

75-
# subcommand functions for the base command
75+
# sub-command functions for the base command
7676
def base_foo(self, args):
77-
"""foo subcommand of base command"""
77+
"""foo sub-command of base command"""
7878
self.poutput(args.x * args.y)
7979

8080
def base_bar(self, args):
81-
"""bar subcommand of base command"""
81+
"""bar sub-command of base command"""
8282
self.poutput('((%s))' % args.z)
8383

8484
def base_sport(self, args):
85-
"""sport subcommand of base command"""
85+
"""sport sub-command of base command"""
8686
self.poutput('Sport is {}'.format(args.sport))
8787

88-
# Set handler functions for the subcommands
88+
# Set handler functions for the sub-commands
8989
parser_foo.set_defaults(func=base_foo)
9090
parser_bar.set_defaults(func=base_bar)
9191
parser_sport.set_defaults(func=base_sport)
@@ -95,21 +95,21 @@ def do_base(self, args):
9595
"""Base command help"""
9696
func = getattr(args, 'func', None)
9797
if func is not None:
98-
# Call whatever subcommand function was selected
98+
# Call whatever sub-command function was selected
9999
func(self, args)
100100
else:
101-
# No subcommand was provided, so call help
101+
# No sub-command was provided, so call help
102102
self.do_help('base')
103103

104104
@cmd2.with_argparser(base2_parser)
105105
def do_alternate(self, args):
106106
"""Alternate command help"""
107107
func = getattr(args, 'func', None)
108108
if func is not None:
109-
# Call whatever subcommand function was selected
109+
# Call whatever sub-command function was selected
110110
func(self, args)
111111
else:
112-
# No subcommand was provided, so call help
112+
# No sub-command was provided, so call help
113113
self.do_help('alternate')
114114

115115

examples/tab_autocomp_dynamic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def query_actors() -> List[str]:
2525

2626

2727
class TabCompleteExample(cmd2.Cmd):
28-
""" Example cmd2 application where we a base command which has a couple subcommands."""
28+
""" Example cmd2 application where we a base command which has a couple sub-commands."""
2929

3030
CAT_AUTOCOMPLETE = 'AutoComplete Examples'
3131

@@ -225,7 +225,7 @@ def _do_vid_media_shows(self, args) -> None:
225225
@cmd2.with_category(CAT_AUTOCOMPLETE)
226226
@cmd2.with_argparser(video_parser)
227227
def do_video(self, args):
228-
"""Video management command demonstrates multiple layers of subcommands being handled by AutoCompleter"""
228+
"""Video management command demonstrates multiple layers of sub-commands being handled by AutoCompleter"""
229229
func = getattr(args, 'func', None)
230230
if func is not None:
231231
# Call whatever subcommand function was selected

0 commit comments

Comments
 (0)