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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
* Enhancements
* Added `read_input()` function that is used to read from stdin. Unlike the Python built-in `input()`, it also has
an argument to disable tab completion while input is being entered.
* Added capability to override the argument parser class used by cmd2 built-in commands. See override_parser.py
example for more details.

## 0.9.20 (November 12, 2019)
* Bug Fixes
Expand Down
12 changes: 11 additions & 1 deletion cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,17 @@
pass

from .ansi import style
from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem
from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem, set_default_argument_parser

# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER
import argparse
cmd2_parser_module = getattr(argparse, 'cmd2_parser_module', None)
if cmd2_parser_module is not None:
import importlib
importlib.import_module(cmd2_parser_module)

# Get the current value for argparse_custom.DEFAULT_ARGUMENT_PARSER
from .argparse_custom import DEFAULT_ARGUMENT_PARSER
from .cmd2 import Cmd, EmptyStatement
from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS
from .decorators import categorize, with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
Expand Down
12 changes: 11 additions & 1 deletion cmd2/argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ def my_completer_method(self, text, line, begidx, endidx, arg_tokens)
import sys
# noinspection PyUnresolvedReferences,PyProtectedMember
from argparse import ZERO_OR_MORE, ONE_OR_MORE, ArgumentError, _
from typing import Callable, Optional, Tuple, Union
from typing import Callable, Optional, Tuple, Type, Union

from .ansi import ansi_aware_write, style_error

Expand Down Expand Up @@ -806,3 +806,13 @@ def _print_message(self, message, file=None):
if file is None:
file = sys.stderr
ansi_aware_write(file, message)


# The default ArgumentParser class for a cmd2 app
DEFAULT_ARGUMENT_PARSER = Cmd2ArgumentParser


def set_default_argument_parser(parser: Type[argparse.ArgumentParser]) -> None:
"""Set the default ArgumentParser class for a cmd2 app"""
global DEFAULT_ARGUMENT_PARSER
DEFAULT_ARGUMENT_PARSER = parser
36 changes: 18 additions & 18 deletions cmd2/cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
from . import constants
from . import plugin
from . import utils
from .argparse_custom import Cmd2ArgumentParser, CompletionItem
from .argparse_custom import CompletionItem, DEFAULT_ARGUMENT_PARSER
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .decorators import with_argparser
from .history import History, HistoryItem
Expand Down Expand Up @@ -2244,7 +2244,7 @@ def _alias_list(self, args: argparse.Namespace) -> None:
"An alias is a command that enables replacement of a word by another string.")
alias_epilog = ("See also:\n"
" macro")
alias_parser = Cmd2ArgumentParser(description=alias_description, epilog=alias_epilog)
alias_parser = DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog)

# Add subcommands to alias
alias_subparsers = alias_parser.add_subparsers(dest='subcommand')
Expand Down Expand Up @@ -2421,7 +2421,7 @@ def _macro_list(self, args: argparse.Namespace) -> None:
"A macro is similar to an alias, but it can contain argument placeholders.")
macro_epilog = ("See also:\n"
" alias")
macro_parser = Cmd2ArgumentParser(description=macro_description, epilog=macro_epilog)
macro_parser = DEFAULT_ARGUMENT_PARSER(description=macro_description, epilog=macro_epilog)

# Add subcommands to macro
macro_subparsers = macro_parser.add_subparsers(dest='subcommand')
Expand Down Expand Up @@ -2537,8 +2537,8 @@ def complete_help_subcommands(self, text: str, line: str, begidx: int, endidx: i
completer = AutoCompleter(argparser, self)
return completer.complete_subcommand_help(tokens, text, line, begidx, endidx)

help_parser = Cmd2ArgumentParser(description="List available commands or provide "
"detailed help for a specific command")
help_parser = DEFAULT_ARGUMENT_PARSER(description="List available commands or provide "
"detailed help for a specific command")
help_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to retrieve help for",
completer_method=complete_help_command)
help_parser.add_argument('subcommands', nargs=argparse.REMAINDER, help="subcommand(s) to retrieve help for",
Expand Down Expand Up @@ -2707,21 +2707,21 @@ def _print_topics(self, header: str, cmds: List[str], verbose: bool) -> None:
command = ''
self.stdout.write("\n")

@with_argparser(Cmd2ArgumentParser(description="List available shortcuts"))
@with_argparser(DEFAULT_ARGUMENT_PARSER(description="List available shortcuts"))
def do_shortcuts(self, _: argparse.Namespace) -> None:
"""List available shortcuts"""
# Sort the shortcut tuples by name
sorted_shortcuts = sorted(self.statement_parser.shortcuts, key=lambda x: self.default_sort_key(x[0]))
result = "\n".join('{}: {}'.format(sc[0], sc[1]) for sc in sorted_shortcuts)
self.poutput("Shortcuts for other commands:\n{}".format(result))

@with_argparser(Cmd2ArgumentParser(epilog=INTERNAL_COMMAND_EPILOG))
@with_argparser(DEFAULT_ARGUMENT_PARSER(epilog=INTERNAL_COMMAND_EPILOG))
def do_eof(self, _: argparse.Namespace) -> bool:
"""Called when <Ctrl>-D is pressed"""
# Return True to stop the command loop
return True

@with_argparser(Cmd2ArgumentParser(description="Exit this application"))
@with_argparser(DEFAULT_ARGUMENT_PARSER(description="Exit this application"))
def do_quit(self, _: argparse.Namespace) -> bool:
"""Exit this application"""
# Return True to stop the command loop
Expand Down Expand Up @@ -2824,7 +2824,7 @@ def _show(self, args: argparse.Namespace, parameter: str = '') -> None:
"Accepts abbreviated parameter names so long as there is no ambiguity.\n"
"Call without arguments for a list of settable parameters with their values.")

set_parser = Cmd2ArgumentParser(description=set_description)
set_parser = DEFAULT_ARGUMENT_PARSER(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('param', nargs=argparse.OPTIONAL, help='parameter to set or view',
Expand Down Expand Up @@ -2869,7 +2869,7 @@ def do_set(self, args: argparse.Namespace) -> None:
if onchange_hook is not None:
onchange_hook(old=orig_value, new=new_value) # pylint: disable=not-callable

shell_parser = Cmd2ArgumentParser(description="Execute a command as if at the OS prompt")
shell_parser = DEFAULT_ARGUMENT_PARSER(description="Execute a command as if at the OS prompt")
shell_parser.add_argument('command', help='the command to run', completer_method=shell_cmd_complete)
shell_parser.add_argument('command_args', nargs=argparse.REMAINDER, help='arguments to pass to command',
completer_method=path_complete)
Expand Down Expand Up @@ -3034,7 +3034,7 @@ def _restore_cmd2_env(self, cmd2_env: _SavedCmd2Env) -> None:
"If you see strange parsing behavior, it's best to just open the Python shell\n"
"by providing no arguments to py and run more complex statements there.")

py_parser = Cmd2ArgumentParser(description=py_description)
py_parser = DEFAULT_ARGUMENT_PARSER(description=py_description)
py_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to run")
py_parser.add_argument('remainder', nargs=argparse.REMAINDER, help="remainder of command")

Expand Down Expand Up @@ -3156,7 +3156,7 @@ def py_quit():

return py_bridge.stop

run_pyscript_parser = Cmd2ArgumentParser(description="Run a Python script file inside the console")
run_pyscript_parser = DEFAULT_ARGUMENT_PARSER(description="Run a Python script file inside the console")
run_pyscript_parser.add_argument('script_path', help='path to the script file', completer_method=path_complete)
run_pyscript_parser.add_argument('script_arguments', nargs=argparse.REMAINDER,
help='arguments to pass to script', completer_method=path_complete)
Expand Down Expand Up @@ -3201,7 +3201,7 @@ def do_run_pyscript(self, args: argparse.Namespace) -> Optional[bool]:

# Only include the do_ipy() method if IPython is available on the system
if ipython_available: # pragma: no cover
@with_argparser(Cmd2ArgumentParser(description="Enter an interactive IPython shell"))
@with_argparser(DEFAULT_ARGUMENT_PARSER(description="Enter an interactive IPython shell"))
def do_ipy(self, _: argparse.Namespace) -> None:
"""Enter an interactive IPython shell"""
from .py_bridge import PyBridge
Expand Down Expand Up @@ -3232,7 +3232,7 @@ def load_ipy(cmd2_app: Cmd, py_bridge: PyBridge):

history_description = "View, run, edit, save, or clear previously entered commands"

history_parser = Cmd2ArgumentParser(description=history_description)
history_parser = DEFAULT_ARGUMENT_PARSER(description=history_description)
history_action_group = history_parser.add_mutually_exclusive_group()
history_action_group.add_argument('-r', '--run', action='store_true', help='run selected history items')
history_action_group.add_argument('-e', '--edit', action='store_true',
Expand Down Expand Up @@ -3543,7 +3543,7 @@ def _generate_transcript(self, history: List[Union[HistoryItem, str]], transcrip
"\n"
" set editor (program-name)")

edit_parser = Cmd2ArgumentParser(description=edit_description)
edit_parser = DEFAULT_ARGUMENT_PARSER(description=edit_description)
edit_parser.add_argument('file_path', nargs=argparse.OPTIONAL,
help="optional path to a file to open in editor", completer_method=path_complete)

Expand Down Expand Up @@ -3584,7 +3584,7 @@ def _current_script_dir(self) -> Optional[str]:
"If the -t/--transcript flag is used, this command instead records\n"
"the output of the script commands to a transcript for testing purposes.\n")

run_script_parser = Cmd2ArgumentParser(description=run_script_description)
run_script_parser = DEFAULT_ARGUMENT_PARSER(description=run_script_description)
run_script_parser.add_argument('-t', '--transcript', metavar='TRANSCRIPT_FILE',
help='record the output of the script as a transcript file',
completer_method=path_complete)
Expand Down Expand Up @@ -3658,8 +3658,8 @@ def do_run_script(self, args: argparse.Namespace) -> Optional[bool]:
relative_run_script_epilog = ("Notes:\n"
" This command is intended to only be used within text file scripts.")

relative_run_script_parser = Cmd2ArgumentParser(description=relative_run_script_description,
epilog=relative_run_script_epilog)
relative_run_script_parser = DEFAULT_ARGUMENT_PARSER(description=relative_run_script_description,
epilog=relative_run_script_epilog)
relative_run_script_parser.add_argument('file_path', help='a file path pointing to a script')

@with_argparser(relative_run_script_parser)
Expand Down
35 changes: 35 additions & 0 deletions examples/custom_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# coding=utf-8
"""
Defines the CustomParser used with override_parser.py example
"""
import sys

from cmd2 import Cmd2ArgumentParser, set_default_argument_parser
from cmd2.ansi import style_warning


# First define the parser
class CustomParser(Cmd2ArgumentParser):
"""Overrides error class"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)

def error(self, message: str) -> None:
"""Custom override that applies custom formatting to the error message"""
lines = message.split('\n')
linum = 0
formatted_message = ''
for line in lines:
if linum == 0:
formatted_message = 'Error: ' + line
else:
formatted_message += '\n ' + line
linum += 1

self.print_usage(sys.stderr)
formatted_message = style_warning(formatted_message)
self.exit(2, '{}\n\n'.format(formatted_message))


# Now set the default parser for a cmd2 app
set_default_argument_parser(CustomParser)
24 changes: 24 additions & 0 deletions examples/override_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env python
# coding=utf-8
# flake8: noqa F402
"""
The standard parser used by cmd2 built-in commands is Cmd2ArgumentParser.
The following code shows how to override it with your own parser class.
"""

# First set a value called argparse.cmd2_parser_module with the module that defines the custom parser
# See the code for custom_parser.py. It simply defines a parser and calls cmd2.set_default_argument_parser()
# with the custom parser's type.
import argparse
argparse.cmd2_parser_module = 'examples.custom_parser'

# Next import stuff from cmd2. It will import your module just before the cmd2.Cmd class file is imported
# and therefore override the parser class it uses on its commands.
from cmd2 import cmd2

if __name__ == '__main__':
import sys
app = cmd2.Cmd(use_ipython=True, persistent_history_file='cmd2_history.dat')
app.locals_in_py = True # Enable access to "self" within the py command
app.debug = True # Show traceback if/when an exception occurs
sys.exit(app.cmdloop())
17 changes: 17 additions & 0 deletions tests/test_argparse_custom.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,20 @@ def test_apcustom_required_options():
parser = Cmd2ArgumentParser()
parser.add_argument('--required_flag', required=True)
assert 'required arguments' in parser.format_help()


def test_override_parser():
import importlib
from cmd2 import DEFAULT_ARGUMENT_PARSER

# The standard parser is Cmd2ArgumentParser
assert DEFAULT_ARGUMENT_PARSER == Cmd2ArgumentParser

# Set our parser module and force a reload of cmd2 so it loads the module
argparse.cmd2_parser_module = 'examples.custom_parser'
importlib.reload(cmd2)
from cmd2 import DEFAULT_ARGUMENT_PARSER

# Verify DEFAULT_ARGUMENT_PARSER is now our CustomParser
from examples.custom_parser import CustomParser
assert DEFAULT_ARGUMENT_PARSER == CustomParser