diff options
-rw-r--r-- | CHANGELOG.md | 15 | ||||
-rwxr-xr-x | cmd2/argparse_completer.py | 5 | ||||
-rw-r--r-- | cmd2/cmd2.py | 30 | ||||
-rw-r--r-- | docs/unfreefeatures.rst | 7 | ||||
-rwxr-xr-x | examples/exit_code.py | 43 | ||||
-rwxr-xr-x | examples/tab_autocomp_dynamic.py | 240 | ||||
-rwxr-xr-x | examples/tab_autocompletion.py | 11 | ||||
-rwxr-xr-x | setup.py | 2 | ||||
-rw-r--r-- | tasks.py | 34 | ||||
-rw-r--r-- | tests/conftest.py | 8 | ||||
-rw-r--r-- | tests/test_autocompletion.py | 6 | ||||
-rw-r--r-- | tests/test_cmd2.py | 96 | ||||
-rw-r--r-- | tox.ini | 2 |
13 files changed, 459 insertions, 40 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e4fd022..9f201664 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ -## 0.9.4 (August TBD, 2018) +## 0.9.5 (TBD, 2018) +* Bug Fixes + * Fixed bug where ``get_all_commands`` could return non-callable attributes +* Enhancements + * Added ``exit_code`` attribute of ``cmd2.Cmd`` class + * Enables applications to return a non-zero exit code when exiting from ``cmdloop`` + * ``ACHelpFormatter`` now inherits from ``argparse.RawTextHelpFormatter`` to make it easier + for formatting help/description text + +## 0.9.4 (August 21, 2018) * Bug Fixes * Fixed bug where ``preparse`` was not getting called * Fixed bug in parsing of multiline commands where matching quote is on another line @@ -7,6 +16,8 @@ framework, see ``docs/hooks.rst`` for details. * New dependency on ``attrs`` third party module * Added ``matches_sorted`` member to support custom sorting of tab-completion matches + * Added [tab_autocomp_dynamic.py](https://github.com/python-cmd2/cmd2/blob/master/examples/tab_autocomp_dynamic.py) example + * Demonstrates updating the argparse object during init instead of during class construction * Deprecations * Deprecated the following hook methods, see ``hooks.rst`` for full details: * ``cmd2.Cmd.preparse()`` - equivalent functionality available @@ -16,7 +27,7 @@ * ``cmd2.Cmd.postparsing_postcmd()`` - equivalent functionality available via ``cmd2.Cmd.register_postcmd_hook()`` -## 0.8.9 (August TBD, 2018) +## 0.8.9 (August 20, 2018) * Bug Fixes * Fixed extra slash that could print when tab completing users on Windows diff --git a/cmd2/argparse_completer.py b/cmd2/argparse_completer.py index 1479a6bf..0e241cd9 100755 --- a/cmd2/argparse_completer.py +++ b/cmd2/argparse_completer.py @@ -695,7 +695,7 @@ class AutoCompleter(object): # noinspection PyCompatibility,PyShadowingBuiltins,PyShadowingBuiltins -class ACHelpFormatter(argparse.HelpFormatter): +class ACHelpFormatter(argparse.RawTextHelpFormatter): """Custom help formatter to configure ordering of help text""" def _format_usage(self, usage, actions, groups, prefix) -> str: @@ -870,9 +870,6 @@ class ACHelpFormatter(argparse.HelpFormatter): result = super()._format_args(action, default_metavar) return result - def _split_lines(self, text: str, width) -> List[str]: - return text.splitlines() - # noinspection PyCompatibility class ACArgumentParser(argparse.ArgumentParser): diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 1cdec0b1..0bfc0037 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -523,6 +523,9 @@ class Cmd(cmd.Cmd): # This boolean flag determines whether or not the cmd2 application can interact with the clipboard self.can_clip = can_clip + # This determines if a non-zero exit code should be used when exiting the application + self.exit_code = None + # ----- Methods related to presenting output to the user ----- @property @@ -1571,7 +1574,8 @@ class Cmd(cmd.Cmd): def get_all_commands(self) -> List[str]: """Returns a list of all commands.""" - return [cur_name[3:] for cur_name in self.get_names() if cur_name.startswith('do_')] + return [name[3:] for name in self.get_names() + if name.startswith('do_') and isinstance(getattr(self, name), Callable)] def get_visible_commands(self) -> List[str]: """Returns a list of commands that have not been hidden.""" @@ -1586,7 +1590,8 @@ class Cmd(cmd.Cmd): def get_help_topics(self) -> List[str]: """ Returns a list of help topics """ - return [name[5:] for name in self.get_names() if name.startswith('help_')] + return [name[5:] for name in self.get_names() + if name.startswith('help_') and isinstance(getattr(self, name), Callable)] def complete_help(self, text: str, line: str, begidx: int, endidx: int) -> List[str]: """ @@ -2480,7 +2485,6 @@ Usage: Usage: unalias [-a] name [name ...] def do_eof(self, _: str) -> bool: """Called when <Ctrl>-D is pressed.""" # End of script should not exit app, but <Ctrl>-D should. - print('') # Required for clearing line when exiting submenu return self._STOP_AND_EXIT def do_quit(self, _: str) -> bool: @@ -2488,7 +2492,7 @@ Usage: Usage: unalias [-a] name [name ...] self._should_quit = True return self._STOP_AND_EXIT - def select(self, opts: Union[str, List[str], List[Tuple[str, Optional[str]]]], prompt: str='Your choice? ') -> str: + def select(self, opts: Union[str, List[str], List[Tuple[Any, Optional[str]]]], prompt: str='Your choice? ') -> str: """Presents a numbered menu to the user. Modelled after the bash shell's SELECT. Returns the item chosen. @@ -2570,18 +2574,19 @@ Usage: Usage: unalias [-a] name [name ...] else: raise LookupError("Parameter '%s' not supported (type 'set' for list of parameters)." % param) - set_parser = ACArgumentParser(formatter_class=argparse.RawTextHelpFormatter) + set_description = "Sets a settable parameter or shows current settings of parameters.\n" + set_description += "\n" + set_description += "Accepts abbreviated parameter names so long as there is no ambiguity.\n" + set_description += "Call without arguments for a list of settable parameters with their values." + + set_parser = ACArgumentParser(description=set_description) set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well') set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter') set_parser.add_argument('settable', nargs=(0, 2), help='[param_name] [value]') @with_argparser(set_parser) def do_set(self, args: argparse.Namespace) -> None: - """Sets a settable parameter or shows current settings of parameters. - - Accepts abbreviated parameter names so long as there is no ambiguity. - Call without arguments for a list of settable parameters with their values. - """ + """Sets a settable parameter or shows current settings of parameters""" try: param_name, val = args.settable val = val.strip() @@ -2896,7 +2901,7 @@ Paths or arguments that contain spaces must be enclosed in quotes embed(banner1=banner, exit_msg=exit_msg) load_ipy(bridge) - history_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) + history_parser = ACArgumentParser() history_parser_group = history_parser.add_mutually_exclusive_group() history_parser_group.add_argument('-r', '--run', action='store_true', help='run selected history items') history_parser_group.add_argument('-e', '--edit', action='store_true', @@ -3244,6 +3249,9 @@ Script should contain one command per line, just like command would be typed in func() self.postloop() + if self.exit_code is not None: + sys.exit(self.exit_code) + ### # # plugin related functions diff --git a/docs/unfreefeatures.rst b/docs/unfreefeatures.rst index 93249425..795c919e 100644 --- a/docs/unfreefeatures.rst +++ b/docs/unfreefeatures.rst @@ -201,3 +201,10 @@ Presents numbered options to user, as bash ``select``. 2. salty Sauce? 2 wheaties with salty sauce, yum! + + +Exit code to shell +================== +The ``self.exit_code`` attribute of your ``cmd2`` application controls +what exit code is sent to the shell when your application exits from +``cmdloop()``. diff --git a/examples/exit_code.py b/examples/exit_code.py new file mode 100755 index 00000000..8ae2d310 --- /dev/null +++ b/examples/exit_code.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# coding=utf-8 +"""A simple example demonstrating the following how to emit a non-zero exit code in your cmd2 application. +""" +import cmd2 +import sys +from typing import List + + +class ReplWithExitCode(cmd2.Cmd): + """ Example cmd2 application where we can specify an exit code when existing.""" + + def __init__(self): + super().__init__() + + @cmd2.with_argument_list + def do_exit(self, arg_list: List[str]) -> bool: + """Exit the application with an optional exit code. + +Usage: exit [exit_code] + Where: + * exit_code - integer exit code to return to the shell +""" + # If an argument was provided + if arg_list: + try: + self.exit_code = int(arg_list[0]) + except ValueError: + self.perror("{} isn't a valid integer exit code".format(arg_list[0])) + self.exit_code = -1 + + self._should_quit = True + return self._STOP_AND_EXIT + + def postloop(self) -> None: + """Hook method executed once when the cmdloop() method is about to return.""" + code = self.exit_code if self.exit_code is not None else 0 + self.poutput('{!r} exiting with code: {}'.format(sys.argv[0], code)) + + +if __name__ == '__main__': + app = ReplWithExitCode() + app.cmdloop() diff --git a/examples/tab_autocomp_dynamic.py b/examples/tab_autocomp_dynamic.py new file mode 100755 index 00000000..2c90b7a2 --- /dev/null +++ b/examples/tab_autocomp_dynamic.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python3 +# coding=utf-8 +""" +A example usage of AutoCompleter with delayed initialization of the argparse object + +Copyright 2018 Eric Lin <anselor@gmail.com> +Released under MIT license, see LICENSE file +""" +import argparse +import itertools +from typing import List + +import cmd2 +from cmd2 import argparse_completer + +actors = ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', 'Alec Guinness', 'Peter Mayhew', + 'Anthony Daniels', 'Adam Driver', 'Daisy Ridley', 'John Boyega', 'Oscar Isaac', + 'Lupita Nyong\'o', 'Andy Serkis', 'Liam Neeson', 'Ewan McGregor', 'Natalie Portman', + 'Jake Lloyd', 'Hayden Christensen', 'Christopher Lee'] + + +def query_actors() -> List[str]: + """Simulating a function that queries and returns a completion values""" + return actors + + +class TabCompleteExample(cmd2.Cmd): + """ Example cmd2 application where we a base command which has a couple subcommands.""" + + CAT_AUTOCOMPLETE = 'AutoComplete Examples' + + def __init__(self): + super().__init__() + + video_types_subparsers = TabCompleteExample.video_parser.add_subparsers(title='Media Types', dest='type') + + vid_movies_parser = argparse_completer.ACArgumentParser(prog='movies') + vid_movies_parser.set_defaults(func=TabCompleteExample._do_vid_media_movies) + + vid_movies_commands_subparsers = vid_movies_parser.add_subparsers(title='Commands', dest='command') + + vid_movies_list_parser = vid_movies_commands_subparsers.add_parser('list') + + vid_movies_list_parser.add_argument('-t', '--title', help='Title Filter') + vid_movies_list_parser.add_argument('-r', '--rating', help='Rating Filter', nargs='+', + choices=TabCompleteExample.ratings_types) + # save a reference to the action object + director_action = vid_movies_list_parser.add_argument('-d', '--director', help='Director Filter') + actor_action = vid_movies_list_parser.add_argument('-a', '--actor', help='Actor Filter', action='append') + + # tag the action objects with completion providers. This can be a collection or a callable + setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.static_list_directors) + setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, query_actors) + + vid_movies_add_parser = vid_movies_commands_subparsers.add_parser('add') + vid_movies_add_parser.add_argument('title', help='Movie Title') + vid_movies_add_parser.add_argument('rating', help='Movie Rating', choices=TabCompleteExample.ratings_types) + + # save a reference to the action object + director_action = vid_movies_add_parser.add_argument('-d', '--director', help='Director', nargs=(1, 2), + required=True) + actor_action = vid_movies_add_parser.add_argument('actor', help='Actors', nargs='*') + + vid_movies_load_parser = vid_movies_commands_subparsers.add_parser('load') + vid_movie_file_action = vid_movies_load_parser.add_argument('movie_file', help='Movie database') + + vid_movies_read_parser = vid_movies_commands_subparsers.add_parser('read') + vid_movie_fread_action = vid_movies_read_parser.add_argument('movie_file', help='Movie database') + + # tag the action objects with completion providers. This can be a collection or a callable + setattr(director_action, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.static_list_directors) + setattr(actor_action, argparse_completer.ACTION_ARG_CHOICES, 'instance_query_actors') + + # tag the file property with a custom completion function 'delimeter_complete' provided by cmd2. + setattr(vid_movie_file_action, argparse_completer.ACTION_ARG_CHOICES, + ('delimiter_complete', + {'delimiter': '/', + 'match_against': TabCompleteExample.file_list})) + setattr(vid_movie_fread_action, argparse_completer.ACTION_ARG_CHOICES, + ('path_complete', [False, False])) + + vid_movies_delete_parser = vid_movies_commands_subparsers.add_parser('delete') + vid_delete_movie_id = vid_movies_delete_parser.add_argument('movie_id', help='Movie ID') + setattr(vid_delete_movie_id, argparse_completer.ACTION_ARG_CHOICES, TabCompleteExample.instance_query_movie_ids) + setattr(vid_delete_movie_id, argparse_completer.ACTION_DESCRIPTIVE_COMPLETION_HEADER, 'Title') + + # Add the 'movies' parser as a parent of sub-parser + video_types_subparsers.add_parser('movies', parents=[vid_movies_parser], add_help=False) + + + + vid_shows_parser = argparse_completer.ACArgumentParser(prog='shows') + vid_shows_parser.set_defaults(func=TabCompleteExample._do_vid_media_shows) + + vid_shows_commands_subparsers = vid_shows_parser.add_subparsers(title='Commands', dest='command') + + vid_shows_list_parser = vid_shows_commands_subparsers.add_parser('list') + + video_types_subparsers.add_parser('shows', parents=[vid_shows_parser], add_help=False) + + + # For mocking a data source for the example commands + ratings_types = ['G', 'PG', 'PG-13', 'R', 'NC-17'] + show_ratings = ['TV-Y', 'TV-Y7', 'TV-G', 'TV-PG', 'TV-14', 'TV-MA'] + static_list_directors = ['J. J. Abrams', 'Irvin Kershner', 'George Lucas', 'Richard Marquand', + 'Rian Johnson', 'Gareth Edwards'] + USER_MOVIE_LIBRARY = ['ROGUE1', 'SW_EP04', 'SW_EP05'] + MOVIE_DATABASE_IDS = ['SW_EP1', 'SW_EP02', 'SW_EP03', 'ROGUE1', 'SW_EP04', + 'SW_EP05', 'SW_EP06', 'SW_EP07', 'SW_EP08', 'SW_EP09'] + MOVIE_DATABASE = {'SW_EP04': {'title': 'Star Wars: Episode IV - A New Hope', + 'rating': 'PG', + 'director': ['George Lucas'], + 'actor': ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', + 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels'] + }, + 'SW_EP05': {'title': 'Star Wars: Episode V - The Empire Strikes Back', + 'rating': 'PG', + 'director': ['Irvin Kershner'], + 'actor': ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', + 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels'] + }, + 'SW_EP06': {'title': 'Star Wars: Episode VI - Return of the Jedi', + 'rating': 'PG', + 'director': ['Richard Marquand'], + 'actor': ['Mark Hamill', 'Harrison Ford', 'Carrie Fisher', + 'Alec Guinness', 'Peter Mayhew', 'Anthony Daniels'] + }, + 'SW_EP1': {'title': 'Star Wars: Episode I - The Phantom Menace', + 'rating': 'PG', + 'director': ['George Lucas'], + 'actor': ['Liam Neeson', 'Ewan McGregor', 'Natalie Portman', 'Jake Lloyd'] + }, + 'SW_EP02': {'title': 'Star Wars: Episode II - Attack of the Clones', + 'rating': 'PG', + 'director': ['George Lucas'], + 'actor': ['Liam Neeson', 'Ewan McGregor', 'Natalie Portman', + 'Hayden Christensen', 'Christopher Lee'] + }, + 'SW_EP03': {'title': 'Star Wars: Episode III - Revenge of the Sith', + 'rating': 'PG-13', + 'director': ['George Lucas'], + 'actor': ['Liam Neeson', 'Ewan McGregor', 'Natalie Portman', + 'Hayden Christensen'] + }, + + } + USER_SHOW_LIBRARY = {'SW_REB': ['S01E01', 'S02E02']} + SHOW_DATABASE_IDS = ['SW_CW', 'SW_TCW', 'SW_REB'] + SHOW_DATABASE = {'SW_CW': {'title': 'Star Wars: Clone Wars', + 'rating': 'TV-Y7', + 'seasons': {1: ['S01E01', 'S01E02', 'S01E03'], + 2: ['S02E01', 'S02E02', 'S02E03']} + }, + 'SW_TCW': {'title': 'Star Wars: The Clone Wars', + 'rating': 'TV-PG', + 'seasons': {1: ['S01E01', 'S01E02', 'S01E03'], + 2: ['S02E01', 'S02E02', 'S02E03']} + }, + 'SW_REB': {'title': 'Star Wars: Rebels', + 'rating': 'TV-Y7', + 'seasons': {1: ['S01E01', 'S01E02', 'S01E03'], + 2: ['S02E01', 'S02E02', 'S02E03']} + }, + } + + file_list = \ + [ + '/home/user/file.db', + '/home/user/file space.db', + '/home/user/another.db', + '/home/other user/maps.db', + '/home/other user/tests.db' + ] + + def instance_query_actors(self) -> List[str]: + """Simulating a function that queries and returns a completion values""" + return actors + + def instance_query_movie_ids(self) -> List[str]: + """Demonstrates showing tabular hinting of tab completion information""" + completions_with_desc = [] + + for movie_id in self.MOVIE_DATABASE_IDS: + if movie_id in self.MOVIE_DATABASE: + movie_entry = self.MOVIE_DATABASE[movie_id] + completions_with_desc.append(argparse_completer.CompletionItem(movie_id, movie_entry['title'])) + + return completions_with_desc + + + ################################################################################### + # The media command demonstrates a completer with multiple layers of subcommands + # - This example demonstrates how to tag a completion attribute on each action, enabling argument + # completion without implementing a complete_COMMAND function + + def _do_vid_media_movies(self, args) -> None: + if not args.command: + self.do_help('video movies') + elif args.command == 'list': + for movie_id in TabCompleteExample.MOVIE_DATABASE: + movie = TabCompleteExample.MOVIE_DATABASE[movie_id] + print('{}\n-----------------------------\n{} ID: {}\nDirector: {}\nCast:\n {}\n\n' + .format(movie['title'], movie['rating'], movie_id, + ', '.join(movie['director']), + '\n '.join(movie['actor']))) + + def _do_vid_media_shows(self, args) -> None: + if not args.command: + self.do_help('video shows') + + elif args.command == 'list': + for show_id in TabCompleteExample.SHOW_DATABASE: + show = TabCompleteExample.SHOW_DATABASE[show_id] + print('{}\n-----------------------------\n{} ID: {}' + .format(show['title'], show['rating'], show_id)) + for season in show['seasons']: + ep_list = show['seasons'][season] + print(' Season {}:\n {}' + .format(season, + '\n '.join(ep_list))) + print() + + video_parser = argparse_completer.ACArgumentParser(prog='video') + + @cmd2.with_category(CAT_AUTOCOMPLETE) + @cmd2.with_argparser(video_parser) + def do_video(self, args): + """Video management command demonstrates multiple layers of subcommands being handled by AutoCompleter""" + func = getattr(args, 'func', None) + if func is not None: + # Call whatever subcommand function was selected + func(self, args) + else: + # No subcommand was provided, so call help + self.do_help('video') + + +if __name__ == '__main__': + app = TabCompleteExample() + app.cmdloop() diff --git a/examples/tab_autocompletion.py b/examples/tab_autocompletion.py index 38972358..6a2e683e 100755 --- a/examples/tab_autocompletion.py +++ b/examples/tab_autocompletion.py @@ -125,7 +125,9 @@ class TabCompleteExample(cmd2.Cmd): # - The help output for arguments with multiple flags or with append=True is more concise # - ACArgumentParser adds the ability to specify ranges of argument counts in 'nargs' - suggest_parser = argparse_completer.ACArgumentParser() + suggest_description = "Suggest command demonstrates argparse customizations.\n" + suggest_description += "See hybrid_suggest and orig_suggest to compare the help output." + suggest_parser = argparse_completer.ACArgumentParser(description=suggest_description) suggest_parser.add_argument('-t', '--type', choices=['movie', 'show'], required=True) suggest_parser.add_argument('-d', '--duration', nargs=(1, 2), action='append', @@ -136,12 +138,7 @@ class TabCompleteExample(cmd2.Cmd): @cmd2.with_category(CAT_AUTOCOMPLETE) @cmd2.with_argparser(suggest_parser) def do_suggest(self, args) -> None: - """Suggest command demonstrates argparse customizations - - See hybrid_suggest and orig_suggest to compare the help output. - - - """ + """Suggest command demonstrates argparse customizations""" if not args.type: self.do_help('suggest') @@ -76,7 +76,7 @@ EXTRAS_REQUIRE = { # for python 3.5 and earlier we need the third party mock module "mock ; python_version<'3.6'", 'pytest', 'codecov', 'pytest-cov', 'pytest-mock', 'tox', 'pylint', - 'sphinx', 'sphinx-rtd-theme', 'sphinx-autobuild', 'invoke', 'twine>=1.11', + 'sphinx<1.7.7', 'sphinx-rtd-theme', 'sphinx-autobuild', 'invoke', 'twine>=1.11', ] } @@ -8,7 +8,9 @@ Make sure you satisfy the following Python module requirements if you are trying - setuptools >= 39.1.0 """ import os +import re import shutil +import sys import invoke @@ -173,6 +175,34 @@ def clean_all(context): pass namespace_clean.add_task(clean_all, 'all') +@invoke.task +def tag(context, name='', message=''): + "Add a Git tag and push it to origin" + # If a tag was provided on the command-line, then add a Git tag and push it to origin + if name: + context.run('git tag -a {} -m {!r}'.format(name, message)) + context.run('git push origin {}'.format(name)) +namespace.add_task(tag) + +@invoke.task() +def validatetag(context): + "Check to make sure that a tag exists for the current HEAD and it looks like a valid version number" + # Validate that a Git tag exists for the current commit HEAD + result = context.run("git describe --exact-match --tags $(git log -n1 --pretty='%h')") + tag = result.stdout.rstrip() + + # Validate that the Git tag appears to be a valid version number + ver_regex = re.compile('(\d+)\.(\d+)\.(\d+)') + match = ver_regex.fullmatch(tag) + if match is None: + print('Tag {!r} does not appear to be a valid version number'.format(tag)) + sys.exit(-1) + else: + print('Tag {!r} appears to be a valid version number'.format(tag)) + + +namespace.add_task(validatetag) + @invoke.task(pre=[clean_all]) def sdist(context): "Create a source distribution" @@ -185,13 +215,13 @@ def wheel(context): context.run('python setup.py bdist_wheel') namespace.add_task(wheel) -@invoke.task(pre=[sdist, wheel]) +@invoke.task(pre=[validatetag, sdist, wheel]) def pypi(context): "Build and upload a distribution to pypi" context.run('twine upload dist/*') namespace.add_task(pypi) -@invoke.task(pre=[sdist, wheel]) +@invoke.task(pre=[validatetag, sdist, wheel]) def pypi_test(context): "Build and upload a distribution to https://test.pypi.org" context.run('twine upload --repository-url https://test.pypi.org/legacy/ dist/*') diff --git a/tests/conftest.py b/tests/conftest.py index 9ca506af..fb049a8c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -42,14 +42,14 @@ load Runs commands in script file that is encoded as either ASCII py Invoke python command, shell, or script pyscript Runs a python script file inside the console quit Exits this application. -set Sets a settable parameter or shows current settings of parameters. +set Sets a settable parameter or shows current settings of parameters shell Execute a command as if at the OS prompt. shortcuts Lists shortcuts (aliases) available. unalias Unsets aliases """ # Help text for the history command -HELP_HISTORY = """usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT | -c] [arg] +HELP_HISTORY = """Usage: history [arg] [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT | -c] View, run, edit, save, or clear previously entered commands. @@ -65,9 +65,9 @@ optional arguments: -r, --run run selected history items -e, --edit edit and then run selected history items -s, --script script format; no separation lines - -o FILE, --output-file FILE + -o, --output-file FILE output commands to a script file - -t TRANSCRIPT, --transcript TRANSCRIPT + -t, --transcript TRANSCRIPT output commands and results to a transcript file -c, --clear clears all history """ diff --git a/tests/test_autocompletion.py b/tests/test_autocompletion.py index e0a71831..8aa26e0e 100644 --- a/tests/test_autocompletion.py +++ b/tests/test_autocompletion.py @@ -19,8 +19,8 @@ def cmd2_app(): SUGGEST_HELP = '''Usage: suggest -t {movie, show} [-h] [-d DURATION{1..2}] -Suggest command demonstrates argparse customizations See hybrid_suggest and -orig_suggest to compare the help output. +Suggest command demonstrates argparse customizations. +See hybrid_suggest and orig_suggest to compare the help output. required arguments: -t, --type {movie, show} @@ -59,7 +59,7 @@ def test_help_required_group(cmd2_app, capsys): assert out1 == out2 assert out1[0].startswith('Usage: suggest') assert out1[1] == '' - assert out1[2].startswith('Suggest command demonstrates argparse customizations ') + assert out1[2].startswith('Suggest command demonstrates argparse customizations.') assert out1 == normalize(SUGGEST_HELP) diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index efdfee7e..85e6c2f8 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -63,7 +63,7 @@ def test_base_argparse_help(base_app, capsys): out2 = run_cmd(base_app, 'help set') assert out1 == out2 - assert out1[0].startswith('usage: set') + assert out1[0].startswith('Usage: set') assert out1[1] == '' assert out1[2].startswith('Sets a settable parameter') @@ -72,10 +72,8 @@ def test_base_invalid_option(base_app, capsys): out, err = capsys.readouterr() out = normalize(out) err = normalize(err) - assert len(err) == 3 - assert len(out) == 15 assert 'Error: unrecognized arguments: -z' in err[0] - assert out[0] == 'usage: set [-h] [-a] [-l] [settable [settable ...]]' + assert out[0] == 'Usage: set settable{0..2} [-h] [-a] [-l]' def test_base_shortcuts(base_app): out = run_cmd(base_app, 'shortcuts') @@ -1252,7 +1250,7 @@ load Runs commands in script file that is encoded as either ASCII py Invoke python command, shell, or script pyscript Runs a python script file inside the console quit Exits this application. -set Sets a settable parameter or shows current settings of parameters. +set Sets a settable parameter or shows current settings of parameters shell Execute a command as if at the OS prompt. shortcuts Lists shortcuts (aliases) available. unalias Unsets aliases @@ -1918,6 +1916,94 @@ def test_bad_history_file_path(capsys, request): assert 'readline cannot read' in err +def test_get_all_commands(base_app): + # Verify that the base app has the expected commands + commands = base_app.get_all_commands() + expected_commands = ['_relative_load', 'alias', 'edit', 'eof', 'eos', 'help', 'history', 'load', 'py', 'pyscript', + 'quit', 'set', 'shell', 'shortcuts', 'unalias'] + assert commands == expected_commands + +def test_get_help_topics(base_app): + # Verify that the base app has no additional help_foo methods + custom_help = base_app.get_help_topics() + assert len(custom_help) == 0 + + +class ReplWithExitCode(cmd2.Cmd): + """ Example cmd2 application where we can specify an exit code when existing.""" + + def __init__(self): + super().__init__() + + @cmd2.with_argument_list + def do_exit(self, arg_list) -> bool: + """Exit the application with an optional exit code. + +Usage: exit [exit_code] + Where: + * exit_code - integer exit code to return to the shell +""" + # If an argument was provided + if arg_list: + try: + self.exit_code = int(arg_list[0]) + except ValueError: + self.perror("{} isn't a valid integer exit code".format(arg_list[0])) + self.exit_code = -1 + + self._should_quit = True + return self._STOP_AND_EXIT + + def postloop(self) -> None: + """Hook method executed once when the cmdloop() method is about to return.""" + code = self.exit_code if self.exit_code is not None else 0 + self.poutput('exiting with code: {}'.format(code)) + +@pytest.fixture +def exit_code_repl(): + app = ReplWithExitCode() + return app + +def test_exit_code_default(exit_code_repl): + # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test + app = exit_code_repl + app.use_rawinput = True + app.stdout = StdOut() + + # 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='exit') + builtins.input = m + + # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args + testargs = ["prog"] + expected = 'exiting with code: 0\n' + with mock.patch.object(sys, 'argv', testargs): + # Run the command loop + app.cmdloop() + out = app.stdout.buffer + assert out == expected + +def test_exit_code_nonzero(exit_code_repl): + # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test + app = exit_code_repl + app.use_rawinput = True + app.stdout = StdOut() + + # 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='exit 23') + builtins.input = m + + # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args + testargs = ["prog"] + expected = 'exiting with code: 23\n' + with mock.patch.object(sys, 'argv', testargs): + # Run the command loop + with pytest.raises(SystemExit): + app.cmdloop() + out = app.stdout.buffer + assert out == expected + + class ColorsApp(cmd2.Cmd): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -12,7 +12,7 @@ setenv = [testenv:docs] basepython = python3.5 deps = - sphinx + sphinx<1.7.7 sphinx-rtd-theme changedir = docs commands = sphinx-build -a -W -T -b html -d {envtmpdir}/doctrees . {envtmpdir}/html |