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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
* It is no longer necessary to set the `prog` attribute of an argparser with subcommands. cmd2 now automatically
sets the prog value of it and all its subparsers so that all usage statements contain the top level command name
and not sys.argv[0].
* Breaking changes
* Some constants were moved from cmd2.py to constants.py
* cmd2 command decorators were moved to decorators.py. If you were importing them via cmd2's __init__.py, then
there will be no issues.

## 0.9.19 (October 14, 2019)
* Bug Fixes
Expand Down
7 changes: 4 additions & 3 deletions cmd2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@

from .ansi import style
from .argparse_custom import Cmd2ArgumentParser, CompletionError, CompletionItem
from .cmd2 import Cmd, Statement, EmptyStatement, categorize
from .cmd2 import with_argument_list, with_argparser, with_argparser_and_unknown_args, with_category
from .constants import DEFAULT_SHORTCUTS
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
from .parsing import Statement
from .py_bridge import CommandResult
293 changes: 36 additions & 257 deletions cmd2/cmd2.py

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions cmd2/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,28 @@
LINE_FEED = '\n'

DEFAULT_SHORTCUTS = {'?': 'help', '!': 'shell', '@': 'run_script', '@@': '_relative_run_script'}

# Used as the command name placeholder in disabled command messages.
COMMAND_NAME = "<COMMAND_NAME>"

# All command functions start with this
COMMAND_FUNC_PREFIX = 'do_'

# All help functions start with this
HELP_FUNC_PREFIX = 'help_'

# All command completer functions start with this
COMPLETER_FUNC_PREFIX = 'complete_'

############################################################################################################
# The following are optional attributes added to do_* command functions
############################################################################################################

# The custom help category a command belongs to
CMD_ATTR_HELP_CATEGORY = 'help_category'

# The argparse parser for the command
CMD_ATTR_ARGPARSER = 'argparser'

# Whether or not tokens are unquoted before sending to argparse
CMD_ATTR_PRESERVE_QUOTES = 'preserve_quotes'
203 changes: 203 additions & 0 deletions cmd2/decorators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# coding=utf-8
"""Decorators for cmd2 commands"""
import argparse
from typing import Callable, Iterable, List, Optional, Union

from . import constants
from .parsing import Statement


def categorize(func: Union[Callable, Iterable[Callable]], category: str) -> None:
"""Categorize a function.

The help command output will group this function under the specified category heading

:param func: function or list of functions to categorize
:param category: category to put it in
"""
if isinstance(func, Iterable):
for item in func:
setattr(item, constants.CMD_ATTR_HELP_CATEGORY, category)
else:
setattr(func, constants.CMD_ATTR_HELP_CATEGORY, category)


def with_category(category: str) -> Callable:
"""A decorator to apply a category to a command function."""
def cat_decorator(func):
categorize(func, category)
return func
return cat_decorator


def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) -> Callable[[List], Optional[bool]]:
"""A decorator to alter the arguments passed to a do_* cmd2 method. Default passes a string of whatever the user
typed. With this decorator, the decorated method will receive a list of arguments parsed from user input.

:param args: Single-element positional argument list containing do_* method this decorator is wrapping
:param preserve_quotes: if True, then argument quotes will not be stripped
:return: function that gets passed a list of argument strings
"""
import functools

def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
_, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
statement,
preserve_quotes)

return func(cmd2_app, parsed_arglist)

command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):]
cmd_wrapper.__doc__ = func.__doc__
return cmd_wrapper

if len(args) == 1 and callable(args[0]):
# noinspection PyTypeChecker
return arg_decorator(args[0])
else:
# noinspection PyTypeChecker
return arg_decorator


# noinspection PyProtectedMember
def set_parser_prog(parser: argparse.ArgumentParser, prog: str):
"""
Recursively set prog attribute of a parser and all of its subparsers so that the root command
is a command name and not sys.argv[0].
:param parser: the parser being edited
:param prog: value for the current parsers prog attribute
"""
# Set the prog value for this parser
parser.prog = prog

# Set the prog value for the parser's subcommands
for action in parser._actions:
if isinstance(action, argparse._SubParsersAction):

# Set the prog value for each subcommand
for sub_cmd, sub_cmd_parser in action.choices.items():
sub_cmd_prog = parser.prog + ' ' + sub_cmd
set_parser_prog(sub_cmd_parser, sub_cmd_prog)

# We can break since argparse only allows 1 group of subcommands per level
break


def with_argparser_and_unknown_args(parser: argparse.ArgumentParser, *,
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
preserve_quotes: bool = False) -> \
Callable[[argparse.Namespace, List], Optional[bool]]:
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given
instance of argparse.ArgumentParser, but also returning unknown args as a list.

:param parser: unique instance of ArgumentParser
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
state data that affects parsing.
:param preserve_quotes: if True, then arguments passed to argparse maintain their quotes
:return: function that gets passed argparse-parsed args in a Namespace and a list of unknown argument strings
A member called __statement__ is added to the Namespace to provide command functions access to the
Statement object. This can be useful if the command function needs to know the command line.

"""
import functools

def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
statement,
preserve_quotes)

if ns_provider is None:
namespace = None
else:
namespace = ns_provider(cmd2_app)

try:
args, unknown = parser.parse_known_args(parsed_arglist, namespace)
except SystemExit:
return
else:
setattr(args, '__statement__', statement)
return func(cmd2_app, args, unknown)

# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):]
set_parser_prog(parser, command_name)

# If the description has not been set, then use the method docstring if one exists
if parser.description is None and func.__doc__:
parser.description = func.__doc__

# Set the command's help text as argparser.description (which can be None)
cmd_wrapper.__doc__ = parser.description

# Set some custom attributes for this command
setattr(cmd_wrapper, constants.CMD_ATTR_ARGPARSER, parser)
setattr(cmd_wrapper, constants.CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)

return cmd_wrapper

# noinspection PyTypeChecker
return arg_decorator


def with_argparser(parser: argparse.ArgumentParser, *,
ns_provider: Optional[Callable[..., argparse.Namespace]] = None,
preserve_quotes: bool = False) -> Callable[[argparse.Namespace], Optional[bool]]:
"""A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments
with the given instance of argparse.ArgumentParser.

:param parser: unique instance of ArgumentParser
:param ns_provider: An optional function that accepts a cmd2.Cmd object as an argument and returns an
argparse.Namespace. This is useful if the Namespace needs to be prepopulated with
state data that affects parsing.
:param preserve_quotes: if True, then arguments passed to argparse maintain their quotes
:return: function that gets passed the argparse-parsed args in a Namespace
A member called __statement__ is added to the Namespace to provide command functions access to the
Statement object. This can be useful if the command function needs to know the command line.
"""
import functools

def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_app, statement: Union[Statement, str]):
statement, parsed_arglist = cmd2_app.statement_parser.get_command_arg_list(command_name,
statement,
preserve_quotes)

if ns_provider is None:
namespace = None
else:
namespace = ns_provider(cmd2_app)

try:
args = parser.parse_args(parsed_arglist, namespace)
except SystemExit:
return
else:
setattr(args, '__statement__', statement)
return func(cmd2_app, args)

# argparser defaults the program name to sys.argv[0], but we want it to be the name of our command
command_name = func.__name__[len(constants.COMMAND_FUNC_PREFIX):]
set_parser_prog(parser, command_name)

# If the description has not been set, then use the method docstring if one exists
if parser.description is None and func.__doc__:
parser.description = func.__doc__

# Set the command's help text as argparser.description (which can be None)
cmd_wrapper.__doc__ = parser.description

# Set some custom attributes for this command
setattr(cmd_wrapper, constants.CMD_ATTR_ARGPARSER, parser)
setattr(cmd_wrapper, constants.CMD_ATTR_PRESERVE_QUOTES, preserve_quotes)

return cmd_wrapper

# noinspection PyTypeChecker
return arg_decorator
8 changes: 4 additions & 4 deletions docs/api/decorators.rst
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
Decorators
==========

.. autofunction:: cmd2.cmd2.with_category
.. autofunction:: cmd2.decorators.with_category

.. autofunction:: cmd2.cmd2.with_argument_list
.. autofunction:: cmd2.decorators.with_argument_list

.. autofunction:: cmd2.cmd2.with_argparser_and_unknown_args
.. autofunction:: cmd2.decorators.with_argparser_and_unknown_args

.. autofunction:: cmd2.cmd2.with_argparser
.. autofunction:: cmd2.decorators.with_argparser
2 changes: 1 addition & 1 deletion docs/api/utility_functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Utility Functions

.. autofunction:: cmd2.utils.strip_quotes

.. autofunction:: cmd2.cmd2.categorize
.. autofunction:: cmd2.decorators.categorize

.. autofunction:: cmd2.utils.center_text

Expand Down
6 changes: 3 additions & 3 deletions docs/features/argument_processing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ Decorators provided by cmd2 for argument processing
``cmd2`` provides the following decorators for assisting with parsing arguments
passed to commands:

.. automethod:: cmd2.cmd2.with_argument_list
.. automethod:: cmd2.decorators.with_argument_list
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it should be autofunction instead of automethod here? Or should it be automethod in the api/decorators.rst file? Or does it just not matter?

:noindex:
.. automethod:: cmd2.cmd2.with_argparser
.. automethod:: cmd2.decorators.with_argparser
:noindex:
.. automethod:: cmd2.cmd2.with_argparser_and_unknown_args
.. automethod:: cmd2.decorators.with_argparser_and_unknown_args
:noindex:

All of these decorators accept an optional **preserve_quotes** argument which
Expand Down
2 changes: 1 addition & 1 deletion examples/help_categories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import argparse

import cmd2
from cmd2.cmd2 import COMMAND_NAME
from cmd2 import COMMAND_NAME


class HelpCategories(cmd2.Cmd):
Expand Down
4 changes: 2 additions & 2 deletions tests/test_cmd2.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from unittest import mock

import cmd2
from cmd2 import ansi, clipboard, constants, plugin, utils
from cmd2 import ansi, clipboard, constants, plugin, utils, COMMAND_NAME
from .conftest import run_cmd, normalize, verify_help_text, HELP_HISTORY
from .conftest import SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, complete_tester

Expand Down Expand Up @@ -2342,7 +2342,7 @@ def test_disabled_command_not_in_history(disable_commands_app):
assert saved_len == len(disable_commands_app.history)

def test_disabled_message_command_name(disable_commands_app):
message_to_print = '{} is currently disabled'.format(cmd2.cmd2.COMMAND_NAME)
message_to_print = '{} is currently disabled'.format(COMMAND_NAME)
disable_commands_app.disable_command('has_helper_funcs', message_to_print)

out, err = run_cmd(disable_commands_app, 'has_helper_funcs')
Expand Down
4 changes: 2 additions & 2 deletions tests/test_completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -187,13 +187,13 @@ def test_default_sort_key(cmd2_app):
begidx = endidx - len(text)

# First do alphabetical sorting
cmd2_app.default_sort_key = cmd2.cmd2.ALPHABETICAL_SORT_KEY
cmd2_app.default_sort_key = cmd2.Cmd.ALPHABETICAL_SORT_KEY
expected = ['1', '11', '2']
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
assert first_match is not None and cmd2_app.completion_matches == expected

# Now switch to natural sorting
cmd2_app.default_sort_key = cmd2.cmd2.NATURAL_SORT_KEY
cmd2_app.default_sort_key = cmd2.Cmd.NATURAL_SORT_KEY
expected = ['1', '2', '11']
first_match = complete_tester(text, line, begidx, endidx, cmd2_app)
assert first_match is not None and cmd2_app.completion_matches == expected
Expand Down