Skip to content

Commit 1cad8d9

Browse files
authored
Merge pull request #812 from python-cmd2/custom_parser
Custom parser
2 parents 8c00d34 + 0d6e9cb commit 1cad8d9

File tree

7 files changed

+118
-20
lines changed

7 files changed

+118
-20
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
* Enhancements
55
* Added `read_input()` function that is used to read from stdin. Unlike the Python built-in `input()`, it also has
66
an argument to disable tab completion while input is being entered.
7+
* Added capability to override the argument parser class used by cmd2 built-in commands. See override_parser.py
8+
example for more details.
79

810
## 0.9.20 (November 12, 2019)
911
* Bug Fixes

cmd2/__init__.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,17 @@
1111
pass
1212

1313
from .ansi import style
14-
from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem
14+
from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem, set_default_argument_parser
15+
16+
# Check if user has defined a module that sets a custom value for argparse_custom.DEFAULT_ARGUMENT_PARSER
17+
import argparse
18+
cmd2_parser_module = getattr(argparse, 'cmd2_parser_module', None)
19+
if cmd2_parser_module is not None:
20+
import importlib
21+
importlib.import_module(cmd2_parser_module)
22+
23+
# Get the current value for argparse_custom.DEFAULT_ARGUMENT_PARSER
24+
from .argparse_custom import DEFAULT_ARGUMENT_PARSER
1525
from .cmd2 import Cmd, EmptyStatement
1626
from .constants import COMMAND_NAME, DEFAULT_SHORTCUTS
1727
from .decorators import categorize, with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category

cmd2/argparse_custom.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ def my_completer_method(self, text, line, begidx, endidx, arg_tokens)
182182
import sys
183183
# noinspection PyUnresolvedReferences,PyProtectedMember
184184
from argparse import ZERO_OR_MORE, ONE_OR_MORE, ArgumentError, _
185-
from typing import Callable, Optional, Tuple, Union
185+
from typing import Callable, Optional, Tuple, Type, Union
186186

187187
from .ansi import ansi_aware_write, style_error
188188

@@ -806,3 +806,13 @@ def _print_message(self, message, file=None):
806806
if file is None:
807807
file = sys.stderr
808808
ansi_aware_write(file, message)
809+
810+
811+
# The default ArgumentParser class for a cmd2 app
812+
DEFAULT_ARGUMENT_PARSER = Cmd2ArgumentParser
813+
814+
815+
def set_default_argument_parser(parser: Type[argparse.ArgumentParser]) -> None:
816+
"""Set the default ArgumentParser class for a cmd2 app"""
817+
global DEFAULT_ARGUMENT_PARSER
818+
DEFAULT_ARGUMENT_PARSER = parser

cmd2/cmd2.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
from . import constants
4848
from . import plugin
4949
from . import utils
50-
from .argparse_custom import Cmd2ArgumentParser, CompletionItem
50+
from .argparse_custom import CompletionItem, DEFAULT_ARGUMENT_PARSER
5151
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
5252
from .decorators import with_argparser
5353
from .history import History, HistoryItem
@@ -2244,7 +2244,7 @@ def _alias_list(self, args: argparse.Namespace) -> None:
22442244
"An alias is a command that enables replacement of a word by another string.")
22452245
alias_epilog = ("See also:\n"
22462246
" macro")
2247-
alias_parser = Cmd2ArgumentParser(description=alias_description, epilog=alias_epilog)
2247+
alias_parser = DEFAULT_ARGUMENT_PARSER(description=alias_description, epilog=alias_epilog)
22482248

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

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

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

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

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

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

2827-
set_parser = Cmd2ArgumentParser(description=set_description)
2827+
set_parser = DEFAULT_ARGUMENT_PARSER(description=set_description)
28282828
set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well')
28292829
set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter')
28302830
set_parser.add_argument('param', nargs=argparse.OPTIONAL, help='parameter to set or view',
@@ -2869,7 +2869,7 @@ def do_set(self, args: argparse.Namespace) -> None:
28692869
if onchange_hook is not None:
28702870
onchange_hook(old=orig_value, new=new_value) # pylint: disable=not-callable
28712871

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

3037-
py_parser = Cmd2ArgumentParser(description=py_description)
3037+
py_parser = DEFAULT_ARGUMENT_PARSER(description=py_description)
30383038
py_parser.add_argument('command', nargs=argparse.OPTIONAL, help="command to run")
30393039
py_parser.add_argument('remainder', nargs=argparse.REMAINDER, help="remainder of command")
30403040

@@ -3156,7 +3156,7 @@ def py_quit():
31563156

31573157
return py_bridge.stop
31583158

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

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

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

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

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

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

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

3661-
relative_run_script_parser = Cmd2ArgumentParser(description=relative_run_script_description,
3662-
epilog=relative_run_script_epilog)
3661+
relative_run_script_parser = DEFAULT_ARGUMENT_PARSER(description=relative_run_script_description,
3662+
epilog=relative_run_script_epilog)
36633663
relative_run_script_parser.add_argument('file_path', help='a file path pointing to a script')
36643664

36653665
@with_argparser(relative_run_script_parser)

examples/custom_parser.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# coding=utf-8
2+
"""
3+
Defines the CustomParser used with override_parser.py example
4+
"""
5+
import sys
6+
7+
from cmd2 import Cmd2ArgumentParser, set_default_argument_parser
8+
from cmd2.ansi import style_warning
9+
10+
11+
# First define the parser
12+
class CustomParser(Cmd2ArgumentParser):
13+
"""Overrides error class"""
14+
def __init__(self, *args, **kwargs) -> None:
15+
super().__init__(*args, **kwargs)
16+
17+
def error(self, message: str) -> None:
18+
"""Custom override that applies custom formatting to the error message"""
19+
lines = message.split('\n')
20+
linum = 0
21+
formatted_message = ''
22+
for line in lines:
23+
if linum == 0:
24+
formatted_message = 'Error: ' + line
25+
else:
26+
formatted_message += '\n ' + line
27+
linum += 1
28+
29+
self.print_usage(sys.stderr)
30+
formatted_message = style_warning(formatted_message)
31+
self.exit(2, '{}\n\n'.format(formatted_message))
32+
33+
34+
# Now set the default parser for a cmd2 app
35+
set_default_argument_parser(CustomParser)

examples/override_parser.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#!/usr/bin/env python
2+
# coding=utf-8
3+
# flake8: noqa F402
4+
"""
5+
The standard parser used by cmd2 built-in commands is Cmd2ArgumentParser.
6+
The following code shows how to override it with your own parser class.
7+
"""
8+
9+
# First set a value called argparse.cmd2_parser_module with the module that defines the custom parser
10+
# See the code for custom_parser.py. It simply defines a parser and calls cmd2.set_default_argument_parser()
11+
# with the custom parser's type.
12+
import argparse
13+
argparse.cmd2_parser_module = 'examples.custom_parser'
14+
15+
# Next import stuff from cmd2. It will import your module just before the cmd2.Cmd class file is imported
16+
# and therefore override the parser class it uses on its commands.
17+
from cmd2 import cmd2
18+
19+
if __name__ == '__main__':
20+
import sys
21+
app = cmd2.Cmd(use_ipython=True, persistent_history_file='cmd2_history.dat')
22+
app.locals_in_py = True # Enable access to "self" within the py command
23+
app.debug = True # Show traceback if/when an exception occurs
24+
sys.exit(app.cmdloop())

tests/test_argparse_custom.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,20 @@ def test_apcustom_required_options():
242242
parser = Cmd2ArgumentParser()
243243
parser.add_argument('--required_flag', required=True)
244244
assert 'required arguments' in parser.format_help()
245+
246+
247+
def test_override_parser():
248+
import importlib
249+
from cmd2 import DEFAULT_ARGUMENT_PARSER
250+
251+
# The standard parser is Cmd2ArgumentParser
252+
assert DEFAULT_ARGUMENT_PARSER == Cmd2ArgumentParser
253+
254+
# Set our parser module and force a reload of cmd2 so it loads the module
255+
argparse.cmd2_parser_module = 'examples.custom_parser'
256+
importlib.reload(cmd2)
257+
from cmd2 import DEFAULT_ARGUMENT_PARSER
258+
259+
# Verify DEFAULT_ARGUMENT_PARSER is now our CustomParser
260+
from examples.custom_parser import CustomParser
261+
assert DEFAULT_ARGUMENT_PARSER == CustomParser

0 commit comments

Comments
 (0)