summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md8
-rw-r--r--CONTRIBUTING.md2
-rwxr-xr-xREADME.md3
-rw-r--r--cmd2/cmd2.py15
-rw-r--r--examples/scripts/save_help_text.py107
-rw-r--r--tests/test_cmd2.py28
6 files changed, 155 insertions, 8 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a7383541..559014e8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,12 @@
-## 0.9.7 (TBD, 2018)
+## 0.9.7 (January 08, 2019)
+* Bug Fixes
+ * Fixed bug when user chooses a zero or negative index when calling ``Cmd.select()``
+ * Restored behavior where ``cmd_echo`` always starts as False in a py script. This was broken in 0.9.5.
* Enhancements
* **cmdloop** now only attempts to register a custom signal handler for SIGINT if running in the main thread
* commands run as a result of ``default_to_shell`` being **True** now run via ``do_shell()`` and are saved
- to history.
+ to history.
+ * Added more tab completion to pyscript command.
* Deletions (potentially breaking changes)
* Deleted ``Cmd.colorize()`` and ``Cmd._colorcodes`` which were deprecated in 0.9.5
* Replaced ``dir_exe_only`` and ``dir_only`` flags in ``path_complete`` with optional ``path_filter`` function
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index c3da497c..95185efe 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -510,7 +510,7 @@ Since 0.9.2, the process of publishing a new release of `cmd2` to [PyPi](https:/
mostly automated. The manual steps are all git operations. Here's the checklist:
1. Make sure you're on the proper branch (almost always **master**)
-1. Make sure all the unit tests pass wih `invoke pypi-test` or `py.test`
+1. Make sure all the unit tests pass with `invoke pytest` or `py.test`
1. Make sure `CHANGELOG.md` describes the version and has the correct release date
1. Add a git tag representing the version number using ``invoke tag x.y.z``
* Where x, y, and z are all small non-negative integers
diff --git a/README.md b/README.md
index 2f1a6a04..97b7e72b 100755
--- a/README.md
+++ b/README.md
@@ -132,6 +132,7 @@ Instructions for implementing each feature follow.
argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
argparser.add_argument('words', nargs='+', help='words to say')
+
@with_argparser(argparser)
def do_speak(self, args):
"""Repeats what you tell me to."""
@@ -253,6 +254,7 @@ class CmdLineApp(cmd2.Cmd):
speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times')
speak_parser.add_argument('words', nargs='+', help='words to say')
+
@cmd2.with_argparser(speak_parser)
def do_speak(self, args):
"""Repeats what you tell me to."""
@@ -274,6 +276,7 @@ class CmdLineApp(cmd2.Cmd):
mumble_parser = argparse.ArgumentParser()
mumble_parser.add_argument('-r', '--repeat', type=int, help='how many times to repeat')
mumble_parser.add_argument('words', nargs='+', help='words to say')
+
@cmd2.with_argparser(mumble_parser)
def do_mumble(self, args):
"""Mumbles what you tell me to."""
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 0184d889..c10974f1 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -2789,6 +2789,8 @@ class Cmd(cmd.Cmd):
try:
choice = int(response)
+ if choice < 1:
+ raise IndexError
result = fulloptions[choice - 1][0]
break
except (ValueError, IndexError):
@@ -2975,6 +2977,10 @@ class Cmd(cmd.Cmd):
:param filename: filename of *.py script file to run
"""
expanded_filename = os.path.expanduser(filename)
+
+ # cmd_echo defaults to False for scripts. The user can always toggle this value in their script.
+ bridge.cmd_echo = False
+
try:
with open(expanded_filename) as f:
interp.runcode(f.read())
@@ -2996,12 +3002,14 @@ class Cmd(cmd.Cmd):
interp = InteractiveConsole(locals=localvars)
interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())')
+ # Check if the user is running a Python statement on the command line
if args.command:
full_command = args.command
if args.remainder:
full_command += ' ' + ' '.join(args.remainder)
- # If running at the CLI, print the output of the command
+ # Set cmd_echo to True so PyscriptBridge statements like: py app('help')
+ # run at the command line will print their output.
bridge.cmd_echo = True
interp.runcode(full_command)
@@ -3119,8 +3127,9 @@ class Cmd(cmd.Cmd):
pyscript_parser = ACArgumentParser()
setattr(pyscript_parser.add_argument('script_path', help='path to the script file'),
ACTION_ARG_CHOICES, ('path_complete',))
- pyscript_parser.add_argument('script_arguments', nargs=argparse.REMAINDER,
- help='arguments to pass to script')
+ setattr(pyscript_parser.add_argument('script_arguments', nargs=argparse.REMAINDER,
+ help='arguments to pass to script'),
+ ACTION_ARG_CHOICES, ('path_complete',))
@with_argparser(pyscript_parser)
def do_pyscript(self, args: argparse.Namespace) -> None:
diff --git a/examples/scripts/save_help_text.py b/examples/scripts/save_help_text.py
new file mode 100644
index 00000000..73434a31
--- /dev/null
+++ b/examples/scripts/save_help_text.py
@@ -0,0 +1,107 @@
+# coding=utf-8
+# flake8: noqa F821
+"""
+A cmd2 script that saves the help text for every command, sub-command, and topic to a file.
+This is meant to be run within a cmd2 session using pyscript.
+"""
+
+import argparse
+import os
+import sys
+from typing import List, TextIO
+
+ASTERISKS = "********************************************************"
+
+
+def get_sub_commands(parser: argparse.ArgumentParser) -> List[str]:
+ """Get a list of sub-commands for an ArgumentParser"""
+ sub_cmds = []
+
+ # Check if this is parser has sub-commands
+ if parser is not None and parser._subparsers is not None:
+
+ # Find the _SubParsersAction for the sub-commands of this parser
+ for action in parser._subparsers._actions:
+ if isinstance(action, argparse._SubParsersAction):
+ for sub_cmd, sub_cmd_parser in action.choices.items():
+ sub_cmds.append(sub_cmd)
+
+ # Look for nested sub-commands
+ for nested_sub_cmd in get_sub_commands(sub_cmd_parser):
+ sub_cmds.append('{} {}'.format(sub_cmd, nested_sub_cmd))
+
+ break
+
+ sub_cmds.sort()
+ return sub_cmds
+
+
+def add_help_to_file(item: str, outfile: TextIO, is_command: bool) -> None:
+ """
+ Write help text for commands and topics to the output file
+ :param item: what is having its help text saved
+ :param outfile: file being written to
+ :param is_command: tells if the item is a command and not just a help topic
+ """
+ if is_command:
+ label = "COMMAND"
+ else:
+ label = "TOPIC"
+
+ header = '{}\n{}: {}\n{}\n'.format(ASTERISKS, label, item, ASTERISKS)
+ outfile.write(header)
+
+ result = app('help {}'.format(item))
+ outfile.write(result.stdout)
+
+
+def main() -> None:
+ """Main function of this script"""
+
+ # Make sure we have access to self
+ if 'self' not in globals():
+ print("Run 'set locals_in_py true' and then rerun this script")
+ return
+
+ # Make sure the user passed in an output file
+ if len(sys.argv) != 2:
+ print("Usage: {} <output_file>".format(os.path.basename(sys.argv[0])))
+ return
+
+ # Open the output file
+ outfile_path = os.path.expanduser(sys.argv[1])
+ try:
+ outfile = open(outfile_path, 'w')
+ except OSError as e:
+ print("Error opening {} because: {}".format(outfile_path, e))
+ return
+
+ # Write the help summary
+ header = '{0}\nSUMMARY\n{0}\n'.format(ASTERISKS)
+ outfile.write(header)
+
+ result = app('help -v')
+ outfile.write(result.stdout)
+
+ # Get a list of all commands and help topics and then filter out duplicates
+ all_commands = set(self.get_all_commands())
+ all_topics = set(self.get_help_topics())
+ to_save = list(all_commands | all_topics)
+ to_save.sort()
+
+ for item in to_save:
+ is_command = item in all_commands
+ add_help_to_file(item, outfile, is_command)
+
+ if is_command:
+ # Add any sub-commands
+ for subcmd in get_sub_commands(getattr(self.cmd_func(item), 'argparser', None)):
+ full_cmd = '{} {}'.format(item, subcmd)
+ add_help_to_file(full_cmd, outfile, is_command)
+
+ outfile.close()
+ print("Output written to {}".format(outfile_path))
+
+
+# Run main function
+main()
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 008f2cc6..350991fa 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1333,11 +1333,11 @@ def test_select_options(select_app):
# And verify the expected output to stdout
assert out == expected
-def test_select_invalid_option(select_app):
+def test_select_invalid_option_too_big(select_app):
# Mock out the input call so we don't actually wait for a user's response on stdin
m = mock.MagicMock(name='input')
# If side_effect is an iterable then each call to the mock will return the next value from the iterable.
- m.side_effect = ['3', '1'] # First pass and invalid selection, then pass a valid one
+ m.side_effect = ['3', '1'] # First pass an invalid selection, then pass a valid one
builtins.input = m
food = 'fish'
@@ -1357,6 +1357,30 @@ def test_select_invalid_option(select_app):
# And verify the expected output to stdout
assert out == expected
+def test_select_invalid_option_too_small(select_app):
+ # Mock out the input call so we don't actually wait for a user's response on stdin
+ m = mock.MagicMock(name='input')
+ # If side_effect is an iterable then each call to the mock will return the next value from the iterable.
+ m.side_effect = ['0', '1'] # First pass an invalid selection, then pass a valid one
+ builtins.input = m
+
+ food = 'fish'
+ out = run_cmd(select_app, "eat {}".format(food))
+ expected = normalize("""
+ 1. sweet
+ 2. salty
+'0' isn't a valid choice. Pick a number between 1 and 2:
+{} with sweet sauce, yum!
+""".format(food))
+
+ # Make sure our mock was called exactly twice with the expected arguments
+ arg = 'Sauce? '
+ calls = [mock.call(arg), mock.call(arg)]
+ m.assert_has_calls(calls)
+
+ # And verify the expected output to stdout
+ assert out == expected
+
def test_select_list_of_strings(select_app):
# Mock out the input call so we don't actually wait for a user's response on stdin
m = mock.MagicMock(name='input', return_value='2')